paint-brush
Tìm hiểu lý do và cách sử dụng di chuyển cơ sở dữ liệu quan hệby@artemsutulov
1,597
1,597

Tìm hiểu lý do và cách sử dụng di chuyển cơ sở dữ liệu quan hệ

Artem Sutulov7m2022/07/16
Read on Terminal Reader
Read this story w/o Javascript

Giới thiệu Khi phát triển các dịch vụ phụ trợ, sẽ dễ dàng tạo ra sự cố nếu tích hợp cơ sở dữ liệu được triển khai không chính xác. Ngày nay, các nhà phát triển chủ yếu sử dụng hai cách tiếp cận: Tạo tự động, 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. Điều đó có nghĩa là trong thực thể Hibernate, chúng tôi thêm cột mới: @Column (name = "accept_notifications", nullable = false) private Boolean getNotifications; 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. Mỗi nhà phát triển yêu cầu một môi trường riêng biệt. Nhưng tốt hơn vào lần tới, bạn 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. Bạn có thể tìm thấy ví dụ hoạt động đầy đủ trên GitHub.
featured image - Tìm hiểu lý do và cách sử dụng di chuyển cơ sở dữ liệu quan hệ
Artem Sutulov HackerNoon profile picture

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:


  • thông tin cơ bản về di cư
  • cách Flyway hoạt động dưới mui xe .

Vấn đề

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:


  1. 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.

  2. 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 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.


Tạo tự động ngủ đông

Để 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 :


Biểu diễn bảng người dùng


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]');

Thêm một cột mới

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.


Bảng người dùng sau khi thêm cột mới


Đ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.


Giá trị mặc định phức tạp hơn cho nhận_bá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:


  1. 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.

  2. Đô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 đồ.

Dung dịch

Đâ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; }

Cách sử dụng quyền di chuyển

  1. Mỗi phần của quá trình di chuyển phải là không quan trọng, có nghĩa là nếu quá trình di chuyển được áp dụng nhiều lần, trạng thái cơ sở dữ liệu vẫn giữ nguyên. Nếu chúng ta bỏ qua điều đó, chúng ta có thể gặp lỗi sau khi khôi phục hoặc không áp dụng các phần dẫn đến thất bại. Có thể dễ dàng đạt được sự lý tưởng trong hầu hết các trường hợp bằng cách thêm các kiểm tra như if not exists / if exists như chúng tôi đã làm ở trên.
  2. Khi viết một số DDL, tốt hơn nên thêm càng nhiều càng tốt càng tốt trong một lần di chuyển, chứ không phải tạo nhiều cái. Lý do chính là tính dễ đọc. Sẽ tốt hơn nếu các thay đổi liên quan, được thực hiện trong một yêu cầu kéo, nằm trong một tệp.
  3. Không thay đổi các di chuyển hiện có. Đó là một điều hiển nhiên nhưng bắt buộc. Sau khi quá trình di chuyển được viết, hợp nhất và triển khai, nó phải được giữ nguyên. Một số thay đổi liên quan phải được thực hiện trong một thay đổi riêng biệt.
  4. Mỗi nhà phát triển yêu cầu một môi trường riêng biệt. Thông thường, đó là một địa phương. Lý do là nếu một số công cụ di cư được áp dụng cho môi trường chia sẻ, chúng sẽ dẫn đến một số thất bại sau này do cách thức hoạt động của các công cụ di cư.
  5. Thật tiện lợi khi có một số thử nghiệm tích hợp chạy tất cả các quá trình di chuyển trên cơ sở dữ liệu thử nghiệm và kiểm tra xem mọi thứ có hoạt động hay không. Nó có thể thực sự hữu ích trong việc xây dựng kiểm tra tính đúng đắn của PR trước khi hợp nhất và có thể tránh được rất nhiều lỗi cơ bản. Trong ví dụ này, có các bài kiểm tra tích hợp thực hiện điều đó.
  6. Tốt hơn nên sử dụng mẫu 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.

Sự kết luậ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:

  • Viết di chuyển Idempotent.
  • Kiểm tra tất cả các lần di chuyển cùng nhau trên cơ sở dữ liệu thử nghiệm bằng cách viết các bài kiểm tra tích hợp.
  • Bao gồm các thay đổi liên quan vào một tệp.
  • Mỗi nhà phát triển cần môi trường DB của riêng họ.
  • Hãy xem kỹ các phiên bản khi viết di chuyển.
  • Đừng thay đổi những cái đã tồn tại.


Bạn có thể tìm thấy ví dụ hoạt động đầy đủ trên GitHub .