Khi phát triển các dịch vụ phụ trợ, thật dễ dàng để tạo ra các vấn đề nếu tích hợp cơ sở dữ liệu được triển khai không chính xác. Bài viết này sẽ cho bạn biết một số phương pháp hay nhất để làm việc với cơ sở dữ liệu quan hệ trong các dịch vụ hiện đại và cũng sẽ cho bạn thấy rằng việc tự động tạo và giữ cho lược đồ cập nhật không phải là một ý tưởng hay.
Tôi sẽ sử dụng Flyway để di chuyển cơ sở dữ liệu, Spring Boot để thiết lập dễ dàng và H2 làm cơ sở dữ liệu ví dụ.
Tôi không đề cập đến thông tin cơ bản về di cư là gì và cách thức hoạt động của chúng. Đây là những bài viết hay từ Flyway:
Cách đây rất lâu, các nhà phát triển đã khởi tạo và cập nhật cơ sở dữ liệu bằng cách áp dụng các tập lệnh riêng biệt với ứng dụng. Tuy nhiên, không ai làm điều đó vào những ngày này vì nó khó phát triển và duy trì ở trạng thái thích hợp, dẫn đến những rắc rối nghiêm trọng.
Ngày nay, các nhà phát triển chủ yếu sử dụng hai cách tiếp cận:
Tự động tạo, ví dụ, JPA hoặc Hibernate - cơ sở dữ liệu khởi tạo và cập nhật bằng cách so sánh các lớp và trạng thái DB hiện tại; nếu cần thay đổi, chúng sẽ được áp dụng.
Di chuyển cơ sở dữ liệu - các nhà phát triển từng bước cập nhật cơ sở dữ liệu và các thay đổi áp dụng tự động khi khởi động, di chuyển cơ sở dữ liệu.
Ngoài ra, nếu chúng ta nói về Spring, có một khởi tạo cơ sở dữ liệu cơ bản, nhưng nó kém nâng cao hơn so với các chất tương tự như Flyway hoặc Liquibase.
Để chứng minh cách nó hoạt động, hãy sử dụng một ví dụ đơn giản. Người dùng bảng có ba trường - id
, user_name
, email
:
Hãy xem một cái được tạo tự động bởi Hibernate.
Thực thể ngủ đông:
@Entity @Table(name = "users") public class User { @Id @GeneratedValue private UUID id; @Column(name = "user_name", length = 64, nullable = false) private String userName; @Column(name = "email", length = 128, nullable = true) private String email; }
Để kích hoạt tính năng giữ cho lược đồ được cập nhật, chúng ta cần hàng này trong cấu hình Spring Boot và nó bắt đầu hoạt động khi khởi động:
jpa.hibernate.ddl-auto=update
Và đăng nhập từ chế độ ngủ đông khi ứng dụng đang khởi động:
Hibernate: create table users (id binary(255) not null, email varchar(128), user_name varchar(64) not null, primary key (id))
Sau khi tạo tự động, nó đã tạo id
dưới dạng binary
với kích thước tối đa là 255 là quá nhiều vì UUID
chỉ bao gồm 36 ký tự. Vì vậy, chúng tôi cần sử dụng loại UUID
thay thế, tuy nhiên, nó không tạo ra theo cách này. Nó có thể được khắc phục bằng cách thêm chú thích này:
@Column(name = "id", columnDefinition = "uuid")
Tuy nhiên, chúng tôi đã viết định nghĩa SQL vào cột, điều này phá vỡ tính trừu tượng từ SQL sang Java.
Và hãy điền vào bảng với một số người dùng:
insert into users (id, user_name, email) values ('297a848d-d406-4055-8a6f-4a4118a44001', 'Artem', null); insert into users (id, user_name, email) values ('921a9d42-bf14-4c3f-9893-60f79cdd0825', 'Antonio', '[email protected]');
Ví dụ, hãy tưởng tượng rằng sau một thời gian, chúng tôi muốn thêm thông báo vào ứng dụng của mình và do đó theo dõi xem người dùng có muốn nhận chúng hay không. Vì vậy, chúng tôi quyết định thêm một cột receive_notifications
cho người dùng bảng và làm cho nó không thể null.
Điều đó có nghĩa là trong thực thể Hibernate, chúng tôi thêm cột mới:
@Column(name = "receive_notifications", nullable = false) private Boolean receiveNotifications;
Sau khi khởi động ứng dụng, chúng tôi thấy lỗi trong nhật ký và không có cột mới. Đó là vì bảng không trống và chúng ta cần đặt giá trị mặc định cho các hàng hiện có:
Error executing DDL "alter table users add column receive_notifications boolean not null" via JDBC Statement
Chúng ta có thể đặt giá trị mặc định bằng cách thêm lại định nghĩa cột SQL:
columnDefinition = "boolean default true"
Và từ nhật ký Hibernate, chúng ta có thể thấy rằng nó đã hoạt động:
Hibernate: alter table users add column receive_notifications boolean default true not null
Tuy nhiên, hãy tưởng tượng rằng chúng ta cần receive_notifications
để trở thành một cái gì đó phức tạp hơn, ví dụ: true hoặc false, tùy thuộc vào việc email có được điền hay không. Không thể thực hiện logic đó chỉ với Hibernate, vì vậy chúng tôi cần di chuyển bất cứ lúc nào.
Tóm lại, những hạn chế chính của phương pháp tiếp cận lược đồ được tạo tự động và cập nhật:
Nó là Java đầu tiên và do đó không linh hoạt về SQL, không thể đoán trước, được định hướng dựa trên Java trước tiên và đôi khi không thực hiện nội dung SQL theo cách bạn mong đợi. Bạn có thể viết một số định nghĩa SQL để thực hiện nó, nhưng nó bị hạn chế so với DDL SQL thuần túy.
Đôi khi không thể cập nhật các bảng hiện có và thực hiện điều gì đó với dữ liệu và dù sao chúng ta cũng cần các tập lệnh SQL. Trong hầu hết các trường hợp, nó kết thúc với việc cập nhật giản đồ tự động và duy trì quá trình di chuyển để cập nhật dữ liệu. Luôn luôn dễ dàng hơn để tránh tự động tạo và thực hiện mọi thứ liên quan đến lớp cơ sở dữ liệu trong quá trình di chuyển.
Ngoài ra, nó không thuận tiện khi phát triển song song vì nó không hỗ trợ lập phiên bản và rất khó để biết điều gì đang xảy ra với lược đồ.
Đây là giao diện của nó mà không cần tự động tạo và cập nhật giản đồ:
Tập lệnh khởi tạo DB:
resource / db /igration / V1__db_initialization.sql
create table if not exists users ( id uuid not null primary key, user_name varchar(64) not null, email varchar(128) );
Làm đầy cơ sở dữ liệu với một số người dùng:
resource / db /igration / V2__users_some_data.sql
insert into users (id, user_name, email) values ('297a848d-d406-4055-8a6f-4a4118a44001', 'Artem', null); insert into users (ID, USER_NAME, EMAIL) values ('921a9d42-bf14-4c3f-9893-60f79cdd0825', 'Antonio', '[email protected]');
Thêm trường mới và đặt giá trị mặc định không tầm thường cho các hàng hiện có:
resources / db /igration / V3__users_add_receive_notification.sql
alter table users add column if not exists receive_notifications boolean; -- It's not a really safe with huge amount of data but good for the example update users set users.receive_notifications = email is not null; alter table users alter column receive_notifications set not null;
Và không có gì ngăn chúng ta sử dụng chế độ ngủ đông nếu chúng ta muốn. Trong cấu hình, chúng ta cần đặt thuộc tính này:
jpa.hibernate.ddl-auto=validate
Bây giờ Hibernate sẽ không tạo ra bất cứ điều gì. Nó sẽ chỉ kiểm tra xem biểu diễn Java có khớp với DB hay không. Hơn nữa, chúng ta không còn cần phải kết hợp một số Java và SQL để tiến hành tạo chế độ Hibernate tự động, vì vậy, nó có thể ngắn gọn và không có thêm trách nhiệm:
@Entity @Table(name = "users") public class User { @Id @Column(name = "id") @GeneratedValue private UUID id; @Column(name = "user_name", length = 64, nullable = false) private String userName; @Column(name = "email", length = 128, nullable = true) private String email; @Column(name = "receive_notifications", nullable = false) private Boolean receiveNotifications; }
if not exists
/ if exists
như chúng tôi đã làm ở trên.V{version+=1}__description.sql
để đặt tên cho các lần di chuyển thay vì sử dụng V{datetime}__description.sql
. Điều thứ hai là thuận tiện và sẽ giúp tránh xung đột số phiên bản trong quá trình phát triển song song. Nhưng đôi khi, xung đột tên sẽ tốt hơn là áp dụng thành công việc di chuyển mà không có nhà phát triển kiểm soát các phiên bản.
Đó là rất nhiều thông tin, nhưng tôi hy vọng bạn sẽ thấy nó hữu ích. Nếu bạn sử dụng tự động tạo / cập nhật lược đồ - hãy xem kỹ những gì đang xảy ra với lược đồ vì nó có thể hoạt động không thể mong đợi. Và luôn luôn là một ý kiến hay khi thêm càng nhiều mô tả càng tốt để tiến hành nó.
Nhưng tốt hơn là lần sau nên cân nhắc việc di chuyển vì nó sẽ giải phóng các thực thể Java, loại bỏ trách nhiệm thừa và mang lại lợi ích cho bạn với nhiều quyền kiểm soát đối với DDL.
Để tổng hợp các phương pháp hay nhất:
Bạn có thể tìm thấy ví dụ hoạt động đầy đủ trên GitHub .