バックエンド サービスを開発する場合、データベース統合が正しく実装されていないと、簡単に問題が発生します。この記事では、最新のサービスでリレーショナル データベースを操作するためのいくつかのベスト プラクティスについて説明し、スキーマを自動的に生成して最新の状態に保つことは、おそらく良い考えではないことを示します。
データベースの移行にはFlywayを使用し、セットアップを簡単にするためにSpring Bootを使用し、サンプル データベースとしてH2を使用します。
移行とは何か、またそのしくみに関する基本的な情報については説明しませんでした。 Flyway の優れた記事を次に示します。
はるか昔、開発者はアプリケーションとは別にスクリプトを適用してデータベースを初期化および更新していました。しかし、適切な状態で開発および維持することが困難であり、深刻なトラブルにつながるため、最近は誰もそれを行いません。
現在、開発者は主に次の 2 つのアプローチを使用しています。
JPAやHibernateなどの自動生成 - クラスと現在の DB 状態を比較することにより、データベースを初期化し、最新の状態に保ちます。変更が必要な場合は適用されます。
データベースの移行 - 開発者はデータベースを段階的に更新し、変更は起動時に自動的に適用され、データベースの移行が行われます。
また、Spring について言えば、すぐに使用できる基本的なデータベースの初期化がありますが、Flyway や Liquibase などの類似物よりもはるかに高度ではありません。
それがどのように機能するかを示すために、簡単な例を使用してみましょう。 id
、 user_name
、 email
の 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 だけで実装することは不可能なので、とにかく移行が必要です。
要約すると、自動的に生成および更新されるスキーマ アプローチの主な欠点は次のとおりです。
それは Java ファーストであり、その結果、SQL に関して柔軟性がなく、予測不可能であり、Java ファースト指向であり、期待どおりに SQL を実行しないことがあります。いくつかの SQL 定義を記述して実行できますが、純粋な SQL DDL と比較すると制限があります。
場合によっては、既存のテーブルを更新してデータを操作することが不可能な場合もありますが、とにかく 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; }
if not exists
/ if exists
のようなチェックを追加することで、冪等性を簡単に実現できます。V{datetime}__description.sql
を使用する代わりに、移行の名前付けにV{version+=1}__description.sql
パターンを使用することをお勧めします。 2 つ目は便利で、並行開発でのバージョン番号の競合を回避するのに役立ちます。ただし、開発者がバージョンを制御せずに移行を正常に適用するよりも、名前の競合がある方がよい場合もあります。
かなりの情報量でしたが、参考になれば幸いです。スキーマの自動生成/更新を使用する場合は、予期しない動作をする可能性があるため、スキーマで何が起こっているかをよく見てください。そして、それを実行するためにできるだけ多くの説明を追加することは常に良い考えです.
ただし、次回は移行を検討することをお勧めします。これにより、Java エンティティが軽減され、余分な責任が取り除かれ、DDL を大幅に制御できるようになるためです。
ベスト プラクティスをまとめると、次のようになります。
GitHubで完全に機能する例を見つけることができます。