Истината на първо място или защо системата трябва да бъде проектирана въз основа на устройството за база данни

Хей Хабр!

Продължаваме да проучваме темата Ява и Пролет, включително на ниво база данни. Днес ви каним да прочетете защо при проектирането на големи приложения структурата на базата данни, а не кодът на Java трябва да е от решаващо значение, как се прави това и какви изключения има от това правило.

В тази доста късна статия ще обясня защо вярвам, че в почти всички случаи моделът на данни в приложение трябва да бъде проектиран „от базата данни“, а не „от възможностите на Java“ (или какъвто и клиентски език да използвате работещ с). Приемайки втория подход, вие се подготвяте за дълъг път на болка и страдание, след като проектът ви започне да расте.

Статията е написана въз основа на един въпрос, дадено на Stack Overflow.

Интересни дискусии за reddit в секции /r/java и /r/програмиране.

Генериране на код

Колко изненадан бях, че има толкова малък сегмент от потребители, които, след като са се запознали с jOOQ, са възмутени от факта, че jOOQ сериозно разчита на генериране на изходен код, за да работи. Никой не ви спира да използвате jOOQ, както сметнете за добре, или ви принуждава да използвате генериране на код. Но начинът по подразбиране (както е описано в ръководството) за работа с jOOQ е, че започвате със (наследена) схема на база данни, извършвате обратно инженерство с помощта на генератора на код jOOQ, за да получите по този начин набор от класове, представляващи вашите таблици, и след това пишете тип -безопасни заявки към тези таблици:

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

Кодът се генерира или ръчно извън сглобката, или ръчно при всяка сглобка. Такава регенерация например може да последва веднага след това Миграция на база данни на Flyway, която също може да се извърши ръчно или автоматично.

Генериране на изходен код

Има различни философии, предимства и недостатъци, свързани с тези подходи за генериране на код - ръчно и автоматично - които няма да обсъждам подробно в тази статия. Но, като цяло, целият смисъл на генерирания код е, че той ни позволява да възпроизведем в Java онази „истина“, която приемаме за даденост, както в рамките на нашата система, така и извън нея. В известен смисъл това е, което компилаторите правят, когато генерират байт код, машинен код или някаква друга форма на изходен код - ние получаваме представяне на нашата "истина" на друг език, независимо от конкретните причини.

Има много такива генератори на кодове. Например, XJC може да генерира Java код въз основа на XSD или WSDL файлове. Принципът винаги е един и същ:

  • Има някаква истина (вътрешна или външна) - например спецификация, модел на данни и т.н.
  • Имаме нужда от локално представяне на тази истина в нашия език за програмиране.

Освен това, почти винаги е препоръчително да се генерира такова представяне, за да се избегне излишък.

Доставчици на типове и обработка на анотации

Забележка: друг, по-модерен и специфичен подход за генериране на код за jOOQ е използването на доставчици на типове, както са имплементирани във F#. В този случай кодът се генерира от компилатора, всъщност на етапа на компилация. По принцип такъв код не съществува в сорс форма. Java има подобни, макар и не толкова елегантни инструменти - процесори за анотация, например, Lombok.

В известен смисъл тук се случват същите неща като в първия случай, с изключение на:

  • Не виждате генерирания код (може би тази ситуация изглежда по-малко отблъскваща за някого?)
  • Трябва да се уверите, че типовете могат да бъдат предоставени, тоест „true“ трябва винаги да е наличен. Това е лесно в случая с Ломбок, който анотира „истина“. Малко по-сложно е с моделите на бази данни, които зависят от постоянно налична жива връзка.

Какъв е проблемът с генерирането на код?

В допълнение към трудния въпрос как най-добре да стартирате генерирането на код - ръчно или автоматично, трябва да споменем и че има хора, които смятат, че генерирането на код изобщо не е необходимо. Оправданието за тази гледна точка, което срещах най-често, е, че след това е трудно да се създаде конвейер за изграждане. Да, наистина е трудно. Възникват допълнителни разходи за инфраструктура. Ако тепърва започвате с конкретен продукт (независимо дали е jOOQ, или JAXB, или Hibernate и т.н.), настройването на производствена среда отнема време, което предпочитате да отделите за изучаване на самия API, за да можете да извлечете стойност от него .

Ако разходите, свързани с разбирането на структурата на генератора, са твърде високи, тогава наистина API е свършил лоша работа по отношение на използваемостта на генератора на код (а по-късно се оказва, че потребителското персонализиране в него също е трудно). Използваемостта трябва да бъде най-висок приоритет за всеки такъв API. Но това е само един аргумент срещу генерирането на код. В противен случай е абсолютно изцяло ръчно да се напише локално представяне на вътрешна или външна истина.

Мнозина ще кажат, че нямат време да направят всичко това. Те изтичат крайните срокове за своя супер продукт. Някой ден ще спретнем монтажните конвейери, ще имаме време. Ще им отговоря:

Истината на първо място или защо системата трябва да бъде проектирана въз основа на устройството за база данни
Оригинал, Алън О'Рурк, публика

Но в Hibernate/JPA е толкова лесно да се пише Java код.

Наистина ли. За Hibernate и неговите потребители това е едновременно благословия и проклятие. В Hibernate можете просто да напишете няколко обекта, като това:

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

И почти всичко е готово. Сега зависи от Hibernate да генерира сложните „подробности“ за това как точно този обект ще бъде дефиниран в DDL на вашия SQL „диалект“:

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

... и започнете да изпълнявате приложението. Наистина страхотна възможност да започнете бързо и да опитате различни неща.

Все пак, моля, позволете ми. Аз лъжех.

  • Ще наложи ли Hibernate действително дефиницията на този първичен ключ?
  • Ще създаде ли Hibernate индекс в TITLE? – Знам със сигурност, че ще ни трябва.
  • Ще направи ли Hibernate точно този ключ идентифициращ в спецификацията за самоличност?

Вероятно не. Ако разработвате проекта си от нулата, винаги е удобно просто да изхвърлите старата база данни и да генерирате нова, веднага щом добавите необходимите анотации. По този начин обектът Книга в крайна сметка ще приеме формата:

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

Готино. Регенерирайте. Отново, в този случай ще бъде много лесно в началото.

Но ще трябва да платите за това по-късно

Рано или късно ще трябва да влезете в производство. Точно тогава този модел ще спре да работи. защото:

В производството вече няма да е възможно, ако е необходимо, да изхвърлите старата база данни и да започнете от нулата. Вашата база данни ще стане наследство.

Отсега нататък и завинаги ще трябва да пишете Скриптове за миграция на DDL, например с помощта на Flyway. Какво ще се случи с вашите субекти в този случай? Можете или да ги адаптирате ръчно (и по този начин да удвоите работното си натоварване), или можете да кажете на Hibernate да ги регенерира вместо вас (колко вероятно е тези, генерирани по този начин, да отговорят на очакванията ви?) Така или иначе губите.

Така че, след като влезете в производство, ще имате нужда от горещи пачове. И те трябва да бъдат пуснати в производство много бързо. Тъй като не сте подготвили и не сте организирали плавен тръбопровод на вашите миграции за производство, вие диво закърпвате всичко. И тогава вече нямате време да направите всичко правилно. И вие критикувате Hibernate, защото винаги някой друг е виновен, само не вие...

Вместо това нещата можеха да се направят по съвсем различен начин от самото начало. Например, поставете кръгли колела на велосипед.

Първо базата данни

Истинската "истина" в схемата на вашата база данни и "суверенитетът" над нея се крие в базата данни. Схемата е дефинирана само в самата база данни и никъде другаде и всеки клиент има копие на тази схема, така че е напълно логично да се наложи съответствие със схемата и нейната цялост, да се направи точно в базата данни - където е информацията съхранени.
Това е стара, дори изтъркана мъдрост. Първичните и уникалните ключове са добри. Външните ключове са добри. Проверката на ограниченията е добра. Твърдения - Глоба.

Освен това това не е всичко. Например, използвайки Oracle, вероятно бихте искали да посочите:

  • В какво таблично пространство е вашата маса?
  • Каква е неговата PCTFREE стойност?
  • Какъв е размерът на кеша във вашата последователност (зад идентификатора)

Това може да не е важно в малки системи, но не е нужно да чакате, докато се преместите в сферата на големите данни – можете да започнете да се възползвате от оптимизациите за съхранение, предоставени от доставчика, като тези, споменати по-горе, много по-рано. Нито един от ORMs, които съм виждал (включително jOOQ), не предоставя достъп до пълния набор от DDL опции, които може да искате да използвате във вашата база данни. ORM предлагат някои инструменти, които ви помагат да пишете DDL.

Но в края на деня една добре проектирана схема е написана на ръка в DDL. Всеки генериран DDL е само приблизителна стойност за него.

Какво ще кажете за клиентския модел?

Както бе споменато по-горе, на клиента ще ви трябва копие на схемата на вашата база данни, клиентският изглед. Излишно е да споменаваме, че този клиентски изглед трябва да е в синхрон с действителния модел. Какъв е най-добрият начин да постигнете това? Използване на генератор на кодове.

Всички бази данни предоставят своята мета информация чрез SQL. Ето как да получите всички таблици от вашата база данни в различни SQL диалекти:

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

Тези заявки (или подобни, в зависимост от това дали трябва да вземете предвид и изгледи, материализирани изгледи, таблични функции) също се изпълняват чрез извикване DatabaseMetaData.getTables() от JDBC или с помощта на мета-модула jOOQ.

От резултатите от такива заявки е сравнително лесно да се генерира каквото и да е клиентско представяне на вашия модел на база данни, независимо каква технология използвате на клиента.

  • Ако използвате JDBC или Spring, можете да създадете набор от низови константи
  • Ако използвате JPA, можете да генерирате самите обекти
  • Ако използвате jOOQ, можете да генерирате мета-модела jOOQ

В зависимост от това колко функционалност се предлага от API на вашия клиент (напр. jOOQ или JPA), генерираният мета модел може да бъде наистина богат и завършен. Вземете, например, възможността за имплицитни присъединявания, въведен в jOOQ 3.11, който разчита на генерирана мета информация за връзките на външния ключ, които съществуват между вашите таблици.

Сега всяко увеличение на базата данни автоматично ще актуализира клиентския код. Представете си например:

ALTER TABLE book RENAME COLUMN title TO book_title;

Наистина ли искате да свършите тази работа два пъти? В никакъв случай. Просто ангажирайте DDL, прекарайте го през конвейера си за изграждане и вземете актуализирания обект:

@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;
}

Или актуализираният клас jOOQ. Повечето DDL промени също засягат семантиката, не само синтаксиса. Следователно може да е полезно да погледнете в компилирания код, за да видите какъв код ще (или би могъл) да бъде засегнат от нарастването на вашата база данни.

Единствената истина

Независимо каква технология използвате, винаги има един модел, който е единственият източник на истина за дадена подсистема - или най-малкото трябва да се стремим към това и да избягваме такова корпоративно объркване, където „истината“ е навсякъде и никъде едновременно . Всичко може да бъде много по-просто. Ако просто обменяте XML файлове с друга система, просто използвайте XSD. Вижте метамодела INFORMATION_SCHEMA от jOOQ в XML форма:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd

  • XSD се разбира добре
  • XSD токенизира XML съдържанието много добре и позволява валидиране на всички клиентски езици
  • XSD е с добри версии и има разширена обратна съвместимост
  • XSD може да се преведе в Java код с помощта на XJC

Последната точка е важна. Когато комуникираме с външна система, използвайки XML съобщения, искаме да сме сигурни, че нашите съобщения са валидни. Това е много лесно да се постигне с помощта на JAXB, XJC и XSD. Би било пълна лудост да мислим, че с подход на проектиране „Java first“, при който ние правим нашите съобщения като Java обекти, че те могат по някакъв начин да бъдат съгласувано картографирани в XML и изпратени към друга система за потребление. XML, генериран по този начин, би бил с много лошо качество, недокументиран и труден за разработване. Ако имаше споразумение за ниво на обслужване (SLA) за такъв интерфейс, веднага щяхме да го прецакаме.

Честно казано, това се случва през цялото време с JSON API, но това е друга история, следващия път ще се карам...

Бази данни: те са едно и също нещо

Когато работите с бази данни, разбирате, че всички те са по същество сходни. Базата притежава своите данни и трябва да управлява схемата. Всички модификации, направени в схемата, трябва да бъдат внедрени директно в DDL, така че единственият източник на истина да бъде актуализиран.

Когато е извършена актуализация на източника, всички клиенти трябва също да актуализират своите копия на модела. Някои клиенти могат да бъдат написани на Java с помощта на jOOQ и Hibernate или JDBC (или и двете). Други клиенти могат да бъдат написани на Perl (пожелаваме им успех), докато други могат да бъдат написани на C#. Няма значение. Основният модел е в базата данни. Моделите, генерирани с помощта на ORM, обикновено са с лошо качество, лошо документирани и трудни за разработване.

Така че не правете грешки. Не правете грешки от самото начало. Работа от базата данни. Изградете конвейер за внедряване, който може да бъде автоматизиран. Активирайте генераторите на кодове, за да улесните копирането на вашия модел на база данни и изхвърлянето му на клиенти. И спрете да се тревожите за генераторите на кодове. Те са добри. С тях ще станете по-продуктивни. Просто трябва да отделите малко време за настройката им от самото начало - и след това ви очакват години на повишена производителност, които ще съставят историята на вашия проект.

Не ми благодари още, по-късно.

Изясняване

За да бъде ясно: тази статия по никакъв начин не препоръчва, че трябва да огънете цялата система (т.е. домейн, бизнес логика и т.н. и т.н.), за да пасне на вашия модел на база данни. Това, което казвам в тази статия е, че клиентският код, който взаимодейства с базата данни, трябва да действа въз основа на модела на базата данни, така че самият той да не възпроизвежда модела на базата данни в състояние „първокласен“. Тази логика обикновено се намира в слоя за достъп до данни на вашия клиент.

В двустепенните архитектури, които все още са запазени на места, такъв модел на системата може да бъде единственият възможен. В повечето системи обаче слоят за достъп до данни ми изглежда като "подсистема", която капсулира модела на базата данни.

изключения

Всяко правило има изключения и вече казах, че подходът за генериране на база данни и изходен код понякога може да бъде неподходящ. Ето няколко такива изключения (вероятно има и други):

  • Когато схемата е неизвестна и трябва да бъде открита. Например вие сте доставчик на инструмент, който помага на потребителите да навигират във всяка диаграма. уф Тук няма генериране на код. Но все пак базата данни е на първо място.
  • Когато верига трябва да се генерира в движение, за да се реши някакъв проблем. Този пример изглежда като леко фантастична версия на модела стойност на атрибута на обекта, т.е. всъщност нямате ясно дефинирана схема. В този случай често дори не можете да сте сигурни, че RDBMS ще ви подхожда.

Изключенията по природа са изключителни. В повечето случаи, включващи използването на RDBMS, схемата е известна предварително, тя се намира в RDBMS и е единственият източник на „истина“ и всички клиенти трябва да придобият копия, извлечени от нея. В идеалния случай трябва да използвате генератор на код.

Източник: www.habr.com

Добавяне на нов коментар