paint-brush
リレーショナル データベースの移行を使用する理由と方法を学ぶ@artemsutulov
1,649 測定値
1,649 測定値

リレーショナル データベースの移行を使用する理由と方法を学ぶ

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

長すぎる; 読むには

はじめに バックエンド サービスを開発する場合、データベース統合が正しく実装されていないと、簡単に問題が発生します。 現在、開発者は主に次の 2 つのアプローチを使用しています。 JPA や Hibernate などの自動生成 - クラスと現在の DB 状態を比較することで、データベースを初期化し、最新の状態に保ちます。変更が必要な場合は適用されます。 つまり、Hibernate エンティティに新しい列を追加します。 @Column(name = "receive_notifications", nullable = false) private Boolean receiveNotifications;アプリを起動すると、ログにエラーが表示され、新しい列は表示されません。 各開発者には個別の環境が必要です。 ただし、次回は移行を検討することをお勧めします。これにより、Java エンティティが軽減され、余分な責任が取り除かれ、DDL を大幅に制御できるようになるためです。 GitHub で完全に機能する例を見つけることができます。
featured image - リレーショナル データベースの移行を使用する理由と方法を学ぶ
Artem Sutulov HackerNoon profile picture

バックエンド サービスを開発する場合、データベース統合が正しく実装されていないと、簡単に問題が発生します。この記事では、最新のサービスでリレーショナル データベースを操作するためのいくつかのベスト プラクティスについて説明し、スキーマを自動的に生成して最新の状態に保つことは、おそらく良い考えではないことを示します。


データベースの移行にはFlywayを使用し、セットアップを簡単にするためにSpring Bootを使用し、サンプル データベースとしてH2を使用します。


移行とは何か、またそのしくみに関する基本的な情報については説明しませんでした。 Flyway の優れた記事を次に示します。


問題

はるか昔、開発者はアプリケーションとは別にスクリプトを適用してデータベースを初期化および更新していました。しかし、適切な状態で開発および維持することが困難であり、深刻なトラブルにつながるため、最近は誰もそれを行いません。


現在、開発者は主に次の 2 つのアプローチを使用しています。


  1. JPAHibernateなどの自動生成 - クラスと現在の DB 状態を比較することにより、データベースを初期化し、最新の状態に保ちます。変更が必要な場合は適用されます。

  2. データベースの移行 - 開発者はデータベースを段階的に更新し、変更は起動時に自動的に適用され、データベースの移行が行われます。

    また、Spring について言えば、すぐに使用できる基本的なデータベースの初期化がありますが、Flyway や Liquibase などの類似物よりもはるかに高度ではありません。


Hibernate 自動生成

それがどのように機能するかを示すために、簡単な例を使用してみましょう。 iduser_nameemailの 3 つのフィールドを持つテーブル users:


ユーザー表の表現


Hibernate によって自動的に生成されたものを見てみましょう。

休止状態のエンティティ:

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

スキーマを最新の状態に保つには、Spring Boot 構成に次の行が必要であり、起動時に開始されます。

 jpa.hibernate.ddl-auto=update

アプリケーションの起動時に休止状態からログを記録します。

Hibernate: create table users (id binary(255) not null, email varchar(128), user_name varchar(64) not null, primary key (id))


自動生成後、 UUIDが 36 文字しかないため、最大サイズ 255 のbinaryとして作成されたidは大きすぎます。そのため、代わりにUUIDタイプを使用する必要がありますが、この方法では生成されません。この注釈を追加することで修正できます。

 @Column(name = "id", columnDefinition = "uuid")


ただし、既に列に SQL 定義を書き込んでいるため、SQL から Java への抽象化が崩れています。


テーブルにユーザーを入力してみましょう。

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

新しい列の追加

たとえば、しばらくしてから通知をアプリに追加し、その結果、ユーザーが通知を受け取りたいかどうかを追跡するとします。したがって、列receive_notificationsをテーブル users に追加し、null 非許容にすることにしました。


新しい列を追加した後のユーザー テーブル


つまり、Hibernate エンティティに新しい列を追加します。

 @Column(name = "receive_notifications", nullable = false) private Boolean receiveNotifications;


アプリを起動すると、ログにエラーが表示され、新しい列は表示されません。これは、テーブルが空ではなく、既存の行にデフォルト値を設定する必要があるためです。

Error executing DDL "alter table users add column receive_notifications boolean not null" via JDBC Statement


SQL 列定義を再度追加することで、デフォルト値を設定できます。

 columnDefinition = "boolean default true"


Hibernate ログから、それが機能したことがわかります。

Hibernate: alter table users add column receive_notifications boolean default true not null


しかし、たとえば、電子メールが満たされているかどうかに応じて、true または false など、 receive_notificationsをより複雑なものにする必要があるとします。そのロジックを Hibernate だけで実装することは不可能なので、とにかく移行が必要です。


receive_notifications のより複雑なデフォルト値


要約すると、自動的に生成および更新されるスキーマ アプローチの主な欠点は次のとおりです。


  1. それは Java ファーストであり、その結果、SQL に関して柔軟性がなく、予測不可能であり、Java ファースト指向であり、期待どおりに SQL を実行しないことがあります。いくつかの SQL 定義を記述して実行できますが、純粋な SQL DDL と比較すると制限があります。

  2. 場合によっては、既存のテーブルを更新してデータを操作することが不可能な場合もありますが、とにかく SQL スクリプトが必要です。ほとんどの場合、最終的に自動スキーマ更新と、データ更新のための移行の維持が行われます。移行でデータベース層に関連するすべてを自動的に生成して実行することを避ける方が常に簡単です。


    また、バージョニングがサポートされていないため、並行開発に関しては不便であり、スキーマで何が起こっているのかを伝えるのは難しい.

解決

スキーマを自動的に生成および更新しない場合は、次のようになります。


DBを初期化するためのスクリプト:

resources/db/migration/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) );


一部のユーザーでデータベースをいっぱいにしています:

resources/db/migration/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]');


新しいフィールドを追加し、自明ではないデフォルト値を既存の行に設定します。

resources/db/migration/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;


また、必要に応じて休止状態を使用することを妨げるものは何もありません。 configs では、このプロパティを設定する必要があります。

 jpa.hibernate.ddl-auto=validate


現在、Hibernate は何も生成しません。 Java 表現が DB と一致するかどうかのみをチェックします。さらに、Hibernate の自動生成を実行するために Java と SQL を混在させる必要がなくなったため、簡潔で余分な責任を負う必要がありません。

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

移行を正しく使用する方法

  1. 移行のすべての部分はべき等でなければなりません。つまり、移行が複数回適用されても、データベースの状態は同じままです。それを無視すると、ロールバック後にエラーが発生したり、失敗につながる部分を適用しない場合があります。ほとんどの場合、上で行ったようにif not exists / if existsのようなチェックを追加することで、冪等性を簡単に実現できます。
  2. DDL を作成するときは、複数の移行を作成するのではなく、1 回の移行で合理的に可能な限り多くを追加することをお勧めします。主な理由は読みやすさです。 1 つのプル リクエストで行われた関連する変更が 1 つのファイルにあると、はるかに優れています。
  3. 既存の移行を変更しないでください。それは明らかですが、必要なものです。移行が作成され、マージされ、展開されたら、そのままにしておく必要があります。一部の関連する変更は、別の変更で行う必要があります。
  4. 各開発者には個別の環境が必要です。通常、それはローカルのものです。その理由は、一部の移行が共有環境に適用される場合、移行ツールの動作方法が原因で、後でいくつかの失敗が発生するためです。
  5. テスト データベースですべての移行を実行し、すべてが機能しているかどうかを確認するいくつかの統合テストがあると便利です。これは、マージ前に PR の正確性をチェックするビルドで非常に便利であり、多くの初歩的なミスを回避できます。この例では、すぐにチェックアウトできる統合テストがあります。
  6. V{datetime}__description.sqlを使用する代わりに、移行の名前付けにV{version+=1}__description.sqlパターンを使用することをお勧めします。 2 つ目は便利で、並行開発でのバージョン番号の競合を回避するのに役立ちます。ただし、開発者がバージョンを制御せずに移行を正常に適用するよりも、名前の競合がある方がよい場合もあります。

結論


かなりの情報量でしたが、参考になれば幸いです。スキーマの自動生成/更新を使用する場合は、予期しない動作をする可能性があるため、スキーマで何が起こっているかをよく見てください。そして、それを実行するためにできるだけ多くの説明を追加することは常に良い考えです.


ただし、次回は移行を検討することをお勧めします。これにより、Java エンティティが軽減され、余分な責任が取り除かれ、DDL を大幅に制御できるようになるためです。


ベスト プラクティスをまとめると、次のようになります。

  • 移行を冪等に書きます。
  • 統合テストを作成して、テスト データベースですべての移行を一緒にテストします。
  • 関連する変更を 1 つのファイルに含めます。
  • 各開発者には、独自の DB 環境が必要です。
  • 移行を作成するときは、バージョンをよく見てください。
  • 既存のものを変更しないでください。


GitHubで完全に機能する例を見つけることができます。