Najpierw prawda, czyli dlaczego system musi być zaprojektowany w oparciu o strukturę bazy danych

Hej Habra!

Kontynuujemy eksplorację tematu Java и Wiosnaw tym na poziomie bazy danych. Dziś proponujemy przeczytać o tym, dlaczego przy projektowaniu dużych aplikacji to właśnie struktura bazy danych, a nie kod Java, powinna mieć decydujące znaczenie, jak to się odbywa i jakie są wyjątki od tej reguły.

W tym dość spóźnionym artykule wyjaśnię, dlaczego uważam, że prawie we wszystkich przypadkach model danych w aplikacji powinien być projektowany „na podstawie bazy danych”, a nie „na podstawie możliwości Javy” (lub dowolnego języka klienckiego, którym się Praca z). Wybierając drugie podejście, wkraczasz na długą ścieżkę bólu i cierpienia, gdy Twój projekt zacznie się rozwijać.

Artykuł został napisany na podstawie jedno pytanie, podany na przepełnieniu stosu.

Ciekawe dyskusje na reddit w sekcjach /r/java и / r / programowanie.

Generowanie kodu

Jakże jestem zaskoczony, że istnieje tak mała grupa użytkowników, którzy po zapoznaniu się z jOOQ są oburzeni faktem, że jOOQ poważnie polega na generowaniu kodu źródłowego do działania. Nikt nie powstrzymuje Cię przed używaniem jOOQ w sposób, jaki uznasz za stosowny, i nikt nie zmusza Cię do generowania kodu. Ale domyślnie (jak opisano w podręczniku), jOOQ działa w ten sposób: zaczynasz od (starszego) schematu bazy danych, poddajesz go inżynierii wstecznej za pomocą generatora kodu jOOQ, aby uzyskać zestaw klas reprezentujących twoje tabele, a następnie piszesz typ- bezpieczne zapytania do tych tabel:

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

Kod jest generowany ręcznie poza kompilacją lub ręcznie przy każdej kompilacji. Na przykład taka regeneracja może nastąpić bezpośrednio po Migracja bazy danych Flyway, którą można również wykonać ręcznie lub automatycznie.

Generowanie kodu źródłowego

Z tymi podejściami do generowania kodu – ręcznymi i automatycznymi – wiążą się różne filozofie, zalety i wady, których nie będę omawiać szczegółowo w tym artykule. Ale ogólnie rzecz biorąc, cały sens wygenerowanego kodu polega na tym, że pozwala on odtworzyć w Javie „prawdę”, którą uważamy za oczywistą, zarówno w naszym systemie, jak i poza nim. W pewnym sensie kompilatory, które generują kod bajtowy, kod maszynowy lub inny rodzaj kodu z kodu źródłowego, robią to samo - otrzymujemy reprezentację naszej „prawdy” w innym języku, niezależnie od konkretnych powodów.

Istnieje wiele takich generatorów kodu. Na przykład, XJC może generować kod Java na podstawie plików XSD lub WSDL. Zasada jest zawsze ta sama:

  • Istnieje pewna prawda (wewnętrzna lub zewnętrzna) - na przykład specyfikacja, model danych itp.
  • Potrzebujemy lokalnej reprezentacji tej prawdy w naszym języku programowania.

Co więcej, prawie zawsze wskazane jest wygenerowanie takiej reprezentacji - w celu uniknięcia redundancji.

Dostawcy typów i przetwarzanie adnotacji

Uwaga: Inne, bardziej nowoczesne i specyficzne podejście do generowania kodu dla jOOQ polega na wykorzystaniu dostawców typów, tak jak są zaimplementowane w F#. W tym przypadku kod jest generowany przez kompilator, a właściwie na etapie kompilacji. Zasadniczo taki kod nie istnieje w postaci kodów źródłowych. W Javie istnieją podobne, choć nie tak eleganckie narzędzia – są to np. procesory adnotacji, Lombok.

W pewnym sensie dzieje się tu to samo, co w pierwszym przypadku, z wyjątkiem:

  • Nie widzisz wygenerowanego kodu (może ta sytuacja nie wydaje się komuś taka odrażająca?)
  • Musisz upewnić się, że typy mogą być dostarczane, to znaczy, że "true" musi być zawsze dostępne. Jest to łatwe w przypadku Lombok, który odnotowuje „prawdę”. Jest to trochę trudniejsze w przypadku modeli baz danych, które zależą od zawsze dostępnego połączenia na żywo.

Jaki jest problem z generowaniem kodu?

Oprócz podchwytliwego pytania, jak lepiej zacząć generowanie kodu - ręcznie czy automatycznie, muszę wspomnieć, że są osoby, które uważają, że generowanie kodu w ogóle nie jest potrzebne. Uzasadnieniem dla tego punktu widzenia, z którym spotykałem się najczęściej, jest to, że trudno jest wtedy ustawić potok kompilacji. Tak, to naprawdę trudne. Istnieją dodatkowe koszty infrastruktury. Jeśli dopiero zaczynasz pracę z konkretnym produktem (czy to jOOQ, JAXB, Hibernate itp.), skonfigurowanie środowiska pracy, które chciałbyś poświęcić na naukę samego API, aby uzyskać z niego wartość, wymaga czasu .

Jeśli koszty związane ze zrozumieniem urządzenia generatora są zbyt wysokie, to faktycznie API źle zrobiło z użytecznością generatora kodu (a w przyszłości okazuje się, że customizacja w nim też jest trudna). Użyteczność powinna być najwyższym priorytetem dla każdego takiego interfejsu API. Ale to tylko jeden argument przeciwko generowaniu kodu. W przeciwnym razie napisz całkowicie ręcznie lokalną reprezentację prawdy wewnętrznej lub zewnętrznej.

Wielu powie, że nie mają czasu na to wszystko. Zbliża się termin ich superproduktu. Kiedyś przeczeszemy przenośniki montażowe, zdążymy. odpowiem im:

Najpierw prawda, czyli dlaczego system musi być zaprojektowany w oparciu o strukturę bazy danych
Oryginalny, Alan O'Rourke, stos publiczności

Ale w Hibernate/JPA tak łatwo jest pisać kod „w Javie”.

Naprawdę. Dla Hibernate i jego użytkowników jest to zarówno dobrodziejstwem, jak i przekleństwem. W Hibernate możesz po prostu napisać kilka jednostek, takich jak ta:

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

I prawie wszystko jest gotowe. Teraz zadaniem Hibernate jest generowanie złożonych „szczegółów” tego, jak dokładnie ta jednostka zostanie zdefiniowana w DDL twojego „dialektu” 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);

... i uruchom aplikację. Naprawdę fajna funkcja umożliwiająca szybkie uruchomienie i wypróbowanie różnych rzeczy.

Jednak pozwól mi. Kłamałem.

  • Czy Hibernate faktycznie wymusi definicję tego nazwanego klucza podstawowego?
  • Czy Hibernate utworzy indeks na TITLE? Wiem na pewno, że tego potrzebujemy.
  • Czy Hibernate uczyni ten klucz kluczem tożsamości w specyfikacji tożsamości?

Prawdopodobnie nie. Jeśli tworzysz swój projekt od zera, zawsze wygodnie jest po prostu odrzucić starą bazę danych i wygenerować nową, gdy tylko dodasz niezbędne adnotacje. Tak więc jednostka Księga ostatecznie przybierze postać:

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

Fajny. Zregenerować. Ponownie, w tym przypadku, na początku będzie to bardzo łatwe.

Ale później będziesz musiał za to zapłacić.

Prędzej czy później będziesz musiał przejść do produkcji. Wtedy model przestaje działać. Ponieważ:

W produkcji nie będzie już możliwe, jeśli to konieczne, odrzucenie starej bazy danych i rozpoczęcie wszystkiego od zera. Twoja baza danych zmieni się w starszą.

Od teraz i na zawsze będziesz musiał pisać Skrypty migracji DDL, np. przy użyciu Flyway. A co stanie się z twoimi podmiotami w takim przypadku? Możesz albo dostosować je ręcznie (i podwoić obciążenie pracą), albo zlecić Hibernate ich regenerację (jak prawdopodobne jest, że wygenerowany w ten sposób spełni Twoje oczekiwania?). Tak czy inaczej, przegrasz.

Tak więc, gdy tylko przejdziesz do produkcji, będziesz potrzebować gorących łat. I trzeba je bardzo szybko wprowadzić do produkcji. Ponieważ nie przygotowałeś i nie zorganizowałeś sprawnego przesyłania swoich migracji do środowiska produkcyjnego, szaleńczo instalujesz poprawki. A potem nie masz czasu, żeby zrobić wszystko dobrze. A ty besztasz Hibernate, bo to zawsze czyjaś wina, tylko nie ty...

Zamiast tego od samego początku wszystko można było zrobić zupełnie inaczej. Na przykład umieść okrągłe koła na rowerze.

Najpierw baza danych

Prawdziwa „prawda” w twoim schemacie bazy danych i „suwerenność” nad nim leży w bazie danych. Schemat jest zdefiniowany tylko w samej bazie danych i nigdzie indziej, a każdy z klientów ma kopię tego schematu, więc jak najbardziej sensowne jest wymuszanie przestrzegania schematu i jego integralności, robienie tego bezpośrednio w bazie danych – tam, gdzie przechowywane są informacje.
To stara, nawet oklepana mądrość. Klucze podstawowe i unikalne są dobre. Klucze obce są w porządku. Sprawdzanie ograniczeń jest dobre. Asercje - Cienki.

I to nie wszystko. Na przykład, korzystając z Oracle, prawdopodobnie chciałbyś określić:

  • W jakim obszarze tabel znajduje się twój stół
  • Jaka jest jej wartość PCTFREE
  • Jaki jest rozmiar pamięci podręcznej w twojej sekwencji (za identyfikatorem)

To wszystko może nie mieć znaczenia w małych systemach, ale nie trzeba czekać na przejście do sfery „big data” – można zacząć korzystać z optymalizacji pamięci masowej dostarczanych przez dostawców, takich jak te wspomniane powyżej, znacznie wcześniej. Żadna z ORM, które widziałem (w tym jOOQ) nie zapewnia dostępu do pełnego zestawu opcji DDL, których możesz chcieć użyć w swojej bazie danych. ORM oferują narzędzia ułatwiające pisanie DDL.

Ale ostatecznie dobrze zaprojektowany schemat jest pisany odręcznie w języku DDL. Każdy wygenerowany DDL jest tylko jego przybliżeniem.

A co z modelem klienta?

Jak wspomniano powyżej, na kliencie będziesz potrzebować kopii schematu bazy danych, widoku klienta. Nie trzeba dodawać, że ten widok klienta musi być zsynchronizowany z rzeczywistym modelem. Jaki jest najlepszy sposób, aby to osiągnąć? Z generatorem kodu.

Wszystkie bazy danych udostępniają swoje metainformacje za pośrednictwem SQL. Oto jak uzyskać wszystkie tabele w różnych dialektach SQL z bazy danych:

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

Te zapytania (lub podobne, w zależności od tego, czy trzeba również uwzględnić widoki, widoki zmaterializowane, funkcje z wartościami przechowywanymi w tabeli) są również wykonywane przez wywołanie DatabaseMetaData.getTables() z JDBC lub przy użyciu metamodułu jOOQ.

Na podstawie wyników takich zapytań stosunkowo łatwo jest wygenerować dowolną reprezentację modelu bazy danych po stronie klienta, bez względu na to, jakiej technologii używasz na kliencie.

  • Jeśli używasz JDBC lub Spring, możesz utworzyć zestaw stałych łańcuchowych
  • Jeśli używasz JPA, możesz wygenerować same jednostki
  • Jeśli używasz jOOQ, możesz wygenerować metamodel jOOQ

W zależności od tego, jakie możliwości oferuje API klienta (np. jOOQ lub JPA), wygenerowany metamodel może być naprawdę bogaty i kompletny. Weźmy na przykład możliwość niejawnych połączeń, wprowadzony w jOOQ 3.11, która opiera się na wygenerowanych metainformacjach o relacjach klucza obcego między tabelami.

Teraz każdy przyrost bazy danych automatycznie zaktualizuje kod klienta. Wyobraź sobie na przykład:

ALTER TABLE book RENAME COLUMN title TO book_title;

Czy naprawdę chciałbyś wykonywać tę pracę dwa razy? W żadnym wypadku. Po prostu zatwierdzamy DDL, uruchamiamy go w potoku kompilacji i otrzymujemy zaktualizowaną jednostkę:

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

Lub zaktualizowana klasa jOOQ. Większość zmian DDL dotyczy również semantyki, a nie tylko składni. Dlatego wygodnie jest zobaczyć w skompilowanym kodzie, na jaki kod wpłynie (lub może mieć) zwiększenie bazy danych.

Jedyna prawda

Bez względu na to, jakiej technologii używasz, zawsze istnieje jeden model, który jest jedynym źródłem prawdy dla jakiegoś podsystemu – a przynajmniej powinniśmy do tego dążyć i unikać zamieszania w przedsiębiorstwie, gdzie „prawda” jest jednocześnie wszędzie i nigdzie. Wszystko może być znacznie łatwiejsze. Jeśli tylko wymieniasz pliki XML z innym systemem, po prostu użyj XSD. Spójrz na meta-model INFORMATION_SCHEMA jOOQ w formie XML:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd

  • XSD jest dobrze rozumiany
  • XSD bardzo dobrze oznacza treść XML i umożliwia walidację we wszystkich językach klienckich
  • XSD jest dobrze wersjonowany i wysoce kompatybilny wstecz
  • XSD można przetłumaczyć na kod Java za pomocą XJC

Ostatni punkt jest ważny. Komunikując się z systemem zewnętrznym za pomocą komunikatów XML, chcemy mieć pewność, że nasze komunikaty są poprawne. Jest to bardzo łatwe do osiągnięcia dzięki JAXB, XJC i XSD. Byłoby czystym szaleństwem sądzić, że w podejściu opartym na Javie, w którym tworzymy nasze komunikaty jako obiekty Java, można je w jakiś sposób przetłumaczyć w formacie XML i przesłać do wykorzystania w innym systemie. XML wygenerowany w ten sposób byłby bardzo słabej jakości, nieudokumentowany i trudny do opracowania. Gdyby istniała umowa dotycząca poziomu jakości usług (SLA) na takim interfejsie, od razu byśmy to schrzanili.

Szczerze mówiąc, dokładnie tak dzieje się przez cały czas z interfejsem API JSON, ale to już inna historia, następnym razem będę się spierać…

Bazy danych: są takie same

Pracując z bazami danych, rozumiesz, że wszystkie są zasadniczo takie same. Baza danych jest właścicielem swoich danych i musi zarządzać schematem. Wszelkie modyfikacje dokonane w schemacie muszą zostać zaimplementowane bezpośrednio w DDL, aby zaktualizować pojedyncze źródło prawdy.

Po wystąpieniu aktualizacji źródłowej wszyscy klienci muszą również zaktualizować swoje kopie modelu. Niektóre klienty mogą być napisane w Javie przy użyciu jOOQ i Hibernate lub JDBC (lub obu). Inne klienty mogą być napisane w Perlu (życzmy im powodzenia), inne w C#. To nie ma znaczenia. Główny model znajduje się w bazie danych. Modele generowane przez ORM są zwykle słabej jakości, słabo udokumentowane i trudne do opracowania.

Więc nie popełniaj błędów. Nie popełniaj błędów od samego początku. Pracuj z bazy danych. Zbuduj potok wdrażania, który można zautomatyzować. Włącz generatory kodu, aby w wygodny sposób skopiować model bazy danych i zrzucić go na klientach. I przestań się martwić o generatory kodu. Oni są dobrzy. Dzięki nim staniesz się bardziej produktywny. Wszystko, co musisz zrobić, to poświęcić trochę czasu na skonfigurowanie ich od samego początku, a będziesz mieć lata lepszej wydajności, aby zbudować historię swojego projektu.

Nie dziękuj mi jeszcze, później.

Wyjaśnienie

Dla jasności: ten artykuł w żaden sposób nie sugeruje, że cały system (tj. domena, logika biznesowa itp.) musi być elastyczny, aby pasował do twojego modelu bazy danych. W tym artykule mówię o tym, że kod klienta, który wchodzi w interakcję z bazą danych, powinien działać w oparciu o model bazy danych, aby nie odtwarzał modelu bazy danych w statusie „pierwszej klasy”. Taka logika jest zwykle zlokalizowana w warstwie dostępu do danych na Twoim kliencie.

W architekturach dwupoziomowych, które miejscami jeszcze się zachowały, taki model systemu może być jedynym możliwym. Jednak w większości systemów warstwa dostępu do danych wydaje mi się „podsystemem”, który zawiera model bazy danych.

Wyjątki

Istnieją wyjątki od każdej reguły i powiedziałem wcześniej, że podejście oparte na bazie danych i generowaniu kodu źródłowego może czasami być nieodpowiednie. Oto kilka takich wyjątków (prawdopodobnie są też inne):

  • Gdy schemat jest nieznany i wymaga otwarcia. Na przykład dostarczasz narzędzie, które pomaga użytkownikom poruszać się po dowolnym diagramie. Uff. Nie ma tutaj generowania kodu. Ale nadal - przede wszystkim baza danych.
  • Kiedy obwód musi być generowany w locie, aby rozwiązać jakiś problem. Ten przykład wydaje się być nieco plisowaną wersją wzoru wartość atrybutu podmiotu, czyli tak naprawdę nie masz dobrze zdefiniowanego schematu. W takim przypadku często nie możesz nawet być pewien, czy RDBMS będzie Ci odpowiadał.

Wyjątki są z natury wyjątkowe. W większości przypadków związanych z wykorzystaniem RDBMS schemat jest znany z góry, znajduje się wewnątrz RDBMS i jest jedynym źródłem „prawdy”, a wszyscy klienci muszą nabywać kopie z niego pochodzące. Idealnie powinno to obejmować generator kodu.

Źródło: www.habr.com

Dodaj komentarz