Sự thật trước tiên, hoặc tại sao hệ thống cần được thiết kế dựa trên thiết bị cơ sở dữ liệu

Này Habr!

Chúng tôi tiếp tục nghiên cứu đề tài Java и Mùa xuân, bao gồm cả ở cấp độ cơ sở dữ liệu. Hôm nay, chúng tôi mời bạn đọc về lý do tại sao, khi thiết kế các ứng dụng lớn, cấu trúc cơ sở dữ liệu chứ không phải mã Java mới có tầm quan trọng mang tính quyết định, cách thực hiện điều này và những ngoại lệ nào đối với quy tắc này.

Trong bài viết khá muộn này, tôi sẽ giải thích lý do tại sao tôi tin rằng trong hầu hết các trường hợp, mô hình dữ liệu trong một ứng dụng nên được thiết kế "từ cơ sở dữ liệu" chứ không phải "từ các khả năng của Java" (hoặc bất kỳ ngôn ngữ máy khách nào mà bạn đang sử dụng. làm việc với). Bằng cách thực hiện cách tiếp cận thứ hai, bạn đang tự đặt ra cho mình một chặng đường dài đầy đau khổ và đau khổ khi dự án của bạn bắt đầu phát triển.

Bài viết được viết dựa trên một câu hỏi, được đưa ra trên Stack Overflow.

Các cuộc thảo luận thú vị trên reddit trong các phần /r/java и / r / lập trình.

Tạo mã

Tôi thật ngạc nhiên khi có một bộ phận nhỏ người dùng sau khi làm quen với jOOQ lại cảm thấy phẫn nộ trước việc jOOQ thực sự dựa vào việc tạo mã nguồn để hoạt động. Không ai ngăn cản bạn sử dụng jOOQ khi bạn thấy phù hợp hoặc buộc bạn phải sử dụng tính năng tạo mã. Nhưng cách làm việc mặc định (như được mô tả trong hướng dẫn) với jOOQ là bạn bắt đầu với một lược đồ cơ sở dữ liệu (cũ), đảo ngược nó bằng cách sử dụng trình tạo mã jOOQ để từ đó có được một tập hợp các lớp đại diện cho các bảng của bạn, sau đó viết kiểu -truy vấn an toàn tới các bảng này:

	for (Record2<String, String> record : DSL.using(configuration)
//   ^^^^^^^^^^^^^^^^^^^^^^^ Информация о типах выведена на 
//   основании сгенерированного кода, на который ссылается приведенное
// ниже условие SELECT 
 
       .select(ACTOR.FIRST_NAME, ACTOR.LAST_NAME)
//           vvvvv ^^^^^^^^^^^^  ^^^^^^^^^^^^^^^ сгенерированные имена
       .from(ACTOR)
       .orderBy(1, 2)) {
    // ...
}

Mã được tạo thủ công bên ngoài cụm lắp ráp hoặc theo cách thủ công tại mỗi cụm lắp ráp. Ví dụ, sự tái sinh như vậy có thể diễn ra ngay sau Di chuyển cơ sở dữ liệu đường bay, cũng có thể được thực hiện thủ công hoặc tự động.

Tạo mã nguồn

Có nhiều triết lý, ưu điểm và nhược điểm khác nhau liên quan đến các phương pháp tạo mã này - thủ công và tự động - mà tôi sẽ không thảo luận chi tiết trong bài viết này. Tuy nhiên, nói chung, toàn bộ mục đích của mã được tạo ra là nó cho phép chúng ta tái tạo trong Java “sự thật” mà chúng ta coi là đương nhiên, trong hoặc ngoài hệ thống của chúng ta. Theo một nghĩa nào đó, đây là những gì trình biên dịch thực hiện khi chúng tạo mã byte, mã máy hoặc một số dạng mã nguồn khác - chúng tôi nhận được sự thể hiện "sự thật" của mình bằng một ngôn ngữ khác, bất kể lý do cụ thể là gì.

Có rất nhiều trình tạo mã như vậy. Ví dụ, XJC có thể tạo mã Java dựa trên tệp XSD hoặc WSDL. Nguyên tắc luôn giống nhau:

  • Có một số sự thật (bên trong hoặc bên ngoài) - ví dụ: thông số kỹ thuật, mô hình dữ liệu, v.v.
  • Chúng tôi cần một sự thể hiện cục bộ về sự thật này bằng ngôn ngữ lập trình của chúng tôi.

Hơn nữa, hầu như luôn luôn nên tạo ra một biểu diễn như vậy để tránh sự dư thừa.

Loại nhà cung cấp và xử lý chú thích

Lưu ý: một cách tiếp cận khác, hiện đại và cụ thể hơn để tạo mã cho jOOQ là sử dụng các nhà cung cấp kiểu, khi chúng được triển khai trong F#. Trong trường hợp này, mã được tạo bởi trình biên dịch, thực tế là ở giai đoạn biên dịch. Về nguyên tắc, mã như vậy không tồn tại ở dạng nguồn. Java có các công cụ tương tự, mặc dù không tinh tế bằng - ví dụ: bộ xử lý chú thích, Lombok.

Theo một nghĩa nào đó, những điều tương tự cũng xảy ra ở đây như trong trường hợp đầu tiên, ngoại trừ:

  • Bạn không nhìn thấy mã được tạo ra (có lẽ tình huống này có vẻ ít gây khó chịu hơn đối với ai đó?)
  • Bạn phải đảm bảo rằng các loại có thể được cung cấp, nghĩa là phải luôn có sẵn "true". Điều này thật dễ dàng trong trường hợp của Lombok, chú thích “sự thật”. Việc này phức tạp hơn một chút với các mô hình cơ sở dữ liệu phụ thuộc vào kết nối trực tiếp có sẵn liên tục.

Vấn đề với việc tạo mã là gì?

Ngoài câu hỏi hóc búa về cách tốt nhất để chạy quá trình tạo mã - thủ công hay tự động, chúng tôi cũng phải đề cập rằng có những người tin rằng việc tạo mã hoàn toàn không cần thiết. Lý do biện minh cho quan điểm này, mà tôi thường gặp nhất, là khi đó rất khó để thiết lập một quy trình xây dựng. Vâng, nó thực sự khó khăn. Chi phí cơ sở hạ tầng bổ sung phát sinh. Nếu bạn mới bắt đầu với một sản phẩm cụ thể (cho dù đó là jOOQ, JAXB hay Hibernate, v.v.), thì việc thiết lập môi trường sản xuất sẽ mất thời gian đến mức bạn muốn dành thời gian tìm hiểu API để có thể trích xuất giá trị từ nó .

Nếu chi phí liên quan đến việc hiểu cấu trúc của trình tạo quá cao thì thực sự API đã làm rất kém khả năng sử dụng của trình tạo mã (và sau này hóa ra việc tùy chỉnh của người dùng trong đó cũng rất khó khăn). Khả năng sử dụng phải là ưu tiên cao nhất đối với bất kỳ API nào như vậy. Nhưng đây chỉ là một lập luận chống lại việc tạo mã. Mặt khác, việc viết một bản trình bày cục bộ về sự thật bên trong hoặc bên ngoài là hoàn toàn hoàn toàn thủ công.

Nhiều người sẽ nói rằng họ không có thời gian để làm tất cả những điều này. Họ sắp hết thời hạn cho Siêu phẩm của mình. Một ngày nào đó chúng ta sẽ dọn dẹp băng chuyền lắp ráp, chúng ta sẽ có thời gian. Tôi sẽ trả lời họ:

Sự thật trước tiên, hoặc tại sao hệ thống cần được thiết kế dựa trên thiết bị cơ sở dữ liệu
Nguyên, Alan O'Rourke, Nhóm khán giả

Nhưng trong Hibernate/JPA việc viết mã Java rất dễ dàng.

Thật sự. Đối với Hibernate và người dùng của nó, đây vừa là một điều may mắn vừa là một lời nguyền. Trong Hibernate bạn có thể chỉ cần viết một vài thực thể như thế này:

	@Entity
class Book {
  @Id
  int id;
  String title;
}

Và hầu như mọi thứ đã sẵn sàng. Bây giờ, Hibernate có nhiệm vụ tạo ra các "chi tiết" phức tạp về cách xác định chính xác thực thể này trong DDL của "phương ngữ" SQL của bạn:

	CREATE TABLE book (
  id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
  title VARCHAR(50),
 
  CONSTRAINT pk_book PRIMARY KEY (id)
);
 
CREATE INDEX i_book_title ON book (title);

... và bắt đầu chạy ứng dụng. Một cơ hội thực sự thú vị để bắt đầu nhanh chóng và thử những điều khác nhau.

Tuy nhiên, xin hãy cho phép tôi. Tôi đã nói dối.

  • Hibernate có thực sự thực thi định nghĩa của khóa chính được đặt tên này không?
  • Hibernate có tạo chỉ mục trong TITLE không? – Tôi biết chắc chắn rằng chúng ta sẽ cần nó.
  • Hibernate có thực hiện chính xác việc xác định khóa này trong Đặc tả nhận dạng không?

Chắc là không. Nếu bạn đang phát triển dự án của mình từ đầu, việc loại bỏ cơ sở dữ liệu cũ và tạo cơ sở dữ liệu mới ngay khi bạn thêm các chú thích cần thiết luôn là điều thuận tiện. Do đó, thực thể Book cuối cùng sẽ có dạng:

	@Entity
@Table(name = "book", indexes = {
  @Index(name = "i_book_title", columnList = "title")
})
class Book {
  @Id
  @GeneratedValue(strategy = IDENTITY)
  int id;
  String title;
}

Mát mẻ. Tái sinh. Một lần nữa, trong trường hợp này mọi việc sẽ rất dễ dàng khi bắt đầu.

Nhưng sau này bạn sẽ phải trả tiền

Sớm hay muộn bạn sẽ phải đi vào sản xuất. Đó là lúc mô hình này sẽ ngừng hoạt động. Bởi vì:

Trong quá trình sản xuất, nếu cần, sẽ không thể loại bỏ cơ sở dữ liệu cũ và bắt đầu lại từ đầu. Cơ sở dữ liệu của bạn sẽ trở thành di sản.

Từ bây giờ và mãi mãi bạn sẽ phải viết Các tập lệnh di chuyển DDL, ví dụ: sử dụng Flyway. Điều gì sẽ xảy ra với các thực thể của bạn trong trường hợp này? Bạn có thể điều chỉnh chúng theo cách thủ công (và do đó tăng gấp đôi khối lượng công việc của bạn) hoặc bạn có thể yêu cầu Hibernate tạo lại chúng cho bạn (những cái được tạo theo cách này có khả năng đáp ứng mong đợi của bạn như thế nào?) Dù thế nào đi nữa, bạn cũng thua.

Vì vậy, một khi bạn bắt đầu sản xuất, bạn sẽ cần các bản vá nóng. Và chúng cần được đưa vào sản xuất thật nhanh chóng. Vì bạn không chuẩn bị và không tổ chức quy trình di chuyển sang sản xuất một cách suôn sẻ nên bạn đã vá lỗi mọi thứ một cách điên cuồng. Và khi đó bạn không còn thời gian để làm mọi thứ một cách chính xác. Và bạn chỉ trích Hibernate, vì đó luôn là lỗi của người khác chứ không phải bạn...

Thay vào đó, mọi việc lẽ ra có thể được thực hiện hoàn toàn khác ngay từ đầu. Ví dụ, đặt bánh xe tròn trên xe đạp.

Cơ sở dữ liệu đầu tiên

"Sự thật" thực sự trong lược đồ cơ sở dữ liệu của bạn và "quyền chủ quyền" đối với nó nằm trong cơ sở dữ liệu. Lược đồ chỉ được xác định trong chính cơ sở dữ liệu và không ở đâu khác, đồng thời mỗi khách hàng có một bản sao của lược đồ này, do đó, việc thực thi việc tuân thủ lược đồ và tính toàn vẹn của nó là hoàn toàn hợp lý, thực hiện ngay trong cơ sở dữ liệu - nơi chứa thông tin. được lưu trữ.
Đây là sự khôn ngoan cũ, thậm chí lỗi thời. Khóa chính và khóa duy nhất đều tốt. Khóa ngoại là tốt. Kiểm tra các hạn chế là tốt. Khẳng định - Khỏe.

Hơn nữa, đó không phải là tất cả. Ví dụ: khi sử dụng Oracle, bạn có thể muốn chỉ định:

  • Bảng của bạn nằm trong không gian bảng nào?
  • Giá trị PCTFREE của nó là bao nhiêu?
  • Kích thước bộ đệm trong chuỗi của bạn (đằng sau id) là bao nhiêu

Điều này có thể không quan trọng trong các hệ thống nhỏ, nhưng bạn không cần phải đợi cho đến khi chuyển sang lĩnh vực dữ liệu lớn—bạn có thể bắt đầu hưởng lợi từ các tối ưu hóa lưu trữ do nhà cung cấp cung cấp như những tối ưu hóa được đề cập ở trên sớm hơn nhiều. Không có ORM nào tôi từng thấy (bao gồm jOOQ) cung cấp quyền truy cập vào bộ tùy chọn DDL đầy đủ mà bạn có thể muốn sử dụng trong cơ sở dữ liệu của mình. ORM cung cấp một số công cụ giúp bạn viết DDL.

Nhưng cuối cùng, một mạch được thiết kế tốt sẽ được viết tay bằng DDL. Bất kỳ DDL nào được tạo ra chỉ là giá trị gần đúng của nó.

Còn mô hình khách hàng thì sao?

Như đã đề cập ở trên, trên máy khách, bạn sẽ cần một bản sao lược đồ cơ sở dữ liệu của mình, chế độ xem máy khách. Không cần phải đề cập, chế độ xem máy khách này phải đồng bộ với mô hình thực tế. Cách tốt nhất để đạt được điều này là gì? Sử dụng trình tạo mã.

Tất cả các cơ sở dữ liệu đều cung cấp thông tin meta của chúng thông qua SQL. Đây là cách lấy tất cả các bảng từ cơ sở dữ liệu của bạn bằng các phương ngữ SQL khác nhau:

	-- H2, HSQLDB, MySQL, PostgreSQL, SQL Server
SELECT table_schema, table_name
FROM information_schema.tables
 
-- DB2
SELECT tabschema, tabname
FROM syscat.tables
 
-- Oracle
SELECT owner, table_name
FROM all_tables
 
-- SQLite
SELECT name
FROM sqlite_master
 
-- Teradata
SELECT databasename, tablename
FROM dbc.tables

Các truy vấn này (hoặc các truy vấn tương tự, tùy thuộc vào việc bạn có phải xem xét các khung nhìn, các khung nhìn cụ thể hóa, các hàm có giá trị bảng) cũng được thực thi bằng cách gọi DatabaseMetaData.getTables() từ JDBC hoặc sử dụng siêu mô-đun jOOQ.

Từ kết quả của các truy vấn như vậy, việc tạo ra bất kỳ biểu diễn phía máy khách nào cho mô hình cơ sở dữ liệu của bạn là tương đối dễ dàng, bất kể bạn sử dụng công nghệ nào trên máy khách.

  • Nếu bạn đang sử dụng JDBC hoặc Spring, bạn có thể tạo một tập hợp các hằng chuỗi
  • Nếu bạn sử dụng JPA, bạn có thể tự tạo các thực thể
  • Nếu bạn sử dụng jOOQ, bạn có thể tạo siêu mô hình jOOQ

Tùy thuộc vào mức độ chức năng được API khách hàng của bạn cung cấp (ví dụ: jOOQ hoặc JPA), mô hình meta được tạo có thể thực sự phong phú và đầy đủ. Lấy ví dụ, khả năng tham gia ngầm, được giới thiệu trong jOOQ 3.11, dựa trên thông tin meta được tạo về các mối quan hệ khóa ngoại tồn tại giữa các bảng của bạn.

Bây giờ mọi sự gia tăng cơ sở dữ liệu sẽ tự động cập nhật mã máy khách. Hãy tưởng tượng ví dụ:

ALTER TABLE book RENAME COLUMN title TO book_title;

Bạn có thực sự muốn làm công việc này hai lần không? Không có trường hợp nào. Chỉ cần cam kết DDL, chạy nó trong quy trình xây dựng của bạn và nhận thực thể được cập nhật:

@Entity
@Table(name = "book", indexes = {
 
  // Вы об этом задумывались?
  @Index(name = "i_book_title", columnList = "book_title")
})
class Book {
  @Id
  @GeneratedValue(strategy = IDENTITY)
  int id;
 
  @Column("book_title")
  String bookTitle;
}

Hoặc lớp jOOQ được cập nhật. Hầu hết các thay đổi DDL cũng ảnh hưởng đến ngữ nghĩa chứ không chỉ cú pháp. Do đó, có thể hữu ích khi xem mã đã biên dịch để xem mã nào sẽ (hoặc có thể) bị ảnh hưởng bởi sự gia tăng cơ sở dữ liệu của bạn.

Sự thật duy nhất

Bất kể bạn sử dụng công nghệ gì, luôn có một mô hình là nguồn sự thật duy nhất cho một số hệ thống con - hoặc, ở mức tối thiểu, chúng ta nên cố gắng đạt được điều này và tránh sự nhầm lẫn như vậy trong doanh nghiệp, trong đó “sự thật” ở khắp mọi nơi và không ở đâu cả cùng một lúc . Mọi thứ có thể đơn giản hơn nhiều. Nếu bạn chỉ trao đổi tệp XML với một số hệ thống khác, chỉ cần sử dụng XSD. Hãy xem siêu mô hình INFORMATION_SCHema từ jOOQ ở dạng XML:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd

  • XSD được hiểu rõ
  • XSD mã hóa nội dung XML rất tốt và cho phép xác thực bằng tất cả các ngôn ngữ máy khách
  • XSD được phiên bản tốt và có khả năng tương thích ngược nâng cao
  • XSD có thể được dịch sang mã Java bằng XJC

Điểm cuối cùng là quan trọng. Khi giao tiếp với hệ thống bên ngoài bằng thông báo XML, chúng tôi muốn chắc chắn rằng thông báo của chúng tôi hợp lệ. Điều này rất dễ đạt được bằng cách sử dụng JAXB, XJC và XSD. Sẽ thật điên rồ khi nghĩ rằng, với cách tiếp cận thiết kế "Java đầu tiên", trong đó chúng ta biến các thông điệp của mình thành các đối tượng Java, thì bằng cách nào đó chúng có thể được ánh xạ mạch lạc tới XML và gửi đến một hệ thống khác để sử dụng. XML được tạo theo cách này sẽ có chất lượng rất kém, không có tài liệu và khó phát triển. Nếu có thỏa thuận cấp độ dịch vụ (SLA) cho giao diện như vậy, chúng tôi sẽ ngay lập tức sửa nó.

Thành thật mà nói, đây là điều thường xuyên xảy ra với API JSON, nhưng đó lại là một câu chuyện khác, lần sau tôi sẽ cãi nhau...

Cơ sở dữ liệu: chúng giống nhau

Khi làm việc với cơ sở dữ liệu, bạn nhận ra rằng về cơ bản chúng đều giống nhau. Cơ sở sở hữu dữ liệu của nó và phải quản lý chương trình. Bất kỳ sửa đổi nào được thực hiện đối với lược đồ phải được triển khai trực tiếp trong DDL để nguồn thông tin chính xác duy nhất được cập nhật.

Khi xảy ra cập nhật nguồn, tất cả khách hàng cũng phải cập nhật bản sao mô hình của họ. Một số ứng dụng khách có thể được viết bằng Java bằng jOOQ và Hibernate hoặc JDBC (hoặc cả hai). Các ứng dụng khách khác có thể được viết bằng Perl (chúng tôi chỉ chúc họ may mắn), trong khi những ứng dụng khác có thể được viết bằng C#. Nó không quan trọng. Mô hình chính nằm trong cơ sở dữ liệu. Các mô hình được tạo bằng ORM thường có chất lượng kém, tài liệu kém và khó phát triển.

Vì vậy, đừng phạm sai lầm. Đừng phạm sai lầm ngay từ đầu. Làm việc từ cơ sở dữ liệu. Xây dựng quy trình triển khai có thể được tự động hóa. Kích hoạt trình tạo mã để dễ dàng sao chép mô hình cơ sở dữ liệu của bạn và kết xuất nó trên máy khách. Và ngừng lo lắng về trình tạo mã. Họ tốt. Với họ, bạn sẽ trở nên năng suất hơn. Bạn chỉ cần dành một chút thời gian để thiết lập chúng ngay từ đầu - và sau đó nhiều năm năng suất tăng lên đang chờ đợi bạn, điều này sẽ tạo nên lịch sử cho dự án của bạn.

Sau này đừng cảm ơn tôi nữa.

làm rõ

Nói rõ hơn: Bài viết này không hề ủng hộ việc bạn cần điều chỉnh toàn bộ hệ thống (tức là miền, logic nghiệp vụ, v.v.) để phù hợp với mô hình cơ sở dữ liệu của bạn. Điều tôi đang nói trong bài viết này là mã máy khách tương tác với cơ sở dữ liệu phải hoạt động trên cơ sở mô hình cơ sở dữ liệu, để bản thân nó không tái tạo mô hình cơ sở dữ liệu ở trạng thái "hạng nhất". Logic này thường nằm ở lớp truy cập dữ liệu trên máy khách của bạn.

Trong các kiến ​​trúc hai cấp vẫn còn được bảo tồn ở một số nơi, mô hình hệ thống như vậy có thể là mô hình duy nhất khả thi. Tuy nhiên, trong hầu hết các hệ thống, lớp truy cập dữ liệu đối với tôi dường như là một "hệ thống con" đóng gói mô hình cơ sở dữ liệu.

Ngoại lệ

Mọi quy tắc đều có ngoại lệ và tôi đã nói rằng cách tiếp cận ưu tiên cơ sở dữ liệu và tạo mã nguồn đôi khi có thể không phù hợp. Dưới đây là một vài trường hợp ngoại lệ như vậy (có thể có những trường hợp khác):

  • Khi lược đồ chưa được biết và cần được khám phá. Ví dụ: bạn là nhà cung cấp công cụ giúp người dùng điều hướng bất kỳ sơ đồ nào. Ờ. Không có việc tạo mã ở đây. Tuy nhiên, cơ sở dữ liệu vẫn là ưu tiên hàng đầu.
  • Khi một mạch phải được tạo ra một cách nhanh chóng để giải quyết một số vấn đề. Ví dụ này có vẻ giống như một phiên bản hơi huyền ảo của mẫu giá trị thuộc tính thực thể, tức là bạn không thực sự có một sơ đồ được xác định rõ ràng. Trong trường hợp này, bạn thậm chí có thể không chắc chắn rằng RDBMS có phù hợp với mình hay không.

Các ngoại lệ về bản chất là ngoại lệ. Trong hầu hết các trường hợp liên quan đến việc sử dụng RDBMS, lược đồ được biết trước, nó nằm trong RDBMS và là nguồn “sự thật” duy nhất và tất cả các máy khách phải có được các bản sao bắt nguồn từ nó. Lý tưởng nhất là bạn cần sử dụng trình tạo mã.

Nguồn: www.habr.com

Thêm một lời nhận xét