システム開発において、楽観ロックや論理削除を用いることがよくあります。
しかし、JHipsterでは楽観ロックや論理削除の実装は自動生成することができません。同じデータに対する複数の更新が競合した際には最後の更新で保存され、データの削除は物理的な削除となります。
この記事では、JHipsterの自動生成後に追加のカラムと実装を加え、楽観ロックや論理削除を行う手法を紹介します。
この記事では、JHipsterのバージョン8.4.0を使用しています。
ベースプロジェクトの作成
楽観ロックや論理削除を追加するため、ベースとなるJHipsterのプロジェクトを作成します。
mkdir myApp && cd myApp
myApp配下に以下の内容でmyApp.jdl
を作成します。楽観ロックや論理削除用のカラムは後で追加するため、最初のエンティティ定義には含めません。
application {
config {
applicationType monolith
authenticationType jwt
baseName myApp
buildTool gradle
cacheProvider no
clientFramework react
clientTheme none
databaseType sql
devDatabaseType h2Disk
enableHibernateCache false
enableSwaggerCodegen false
enableTranslation true
feignClient false
languages [en]
messageBroker false
nativeLanguage en
packageName com.mycompany.myapp
prodDatabaseType postgresql
reactive false
searchEngine false
serviceDiscoveryType false
testFrameworks []
websocket false
withAdminUi true
}
entities *
}
entity Employee {
FirstName String,
LastName String,
}
service * with serviceClass
paginate * with pagination
filter *
作成したJDLファイルを指定してアプリケーションを生成します。
jhipster jdl myApp.jdl
楽観ロック
自動生成したソースコードに対してversion
カラムを追加して、楽観ロックを実現します。
LiquibaseによるDBへのカラム追加
JHipsterはデータベースの管理にLiquibaseを使用しています。Liquibaseについては過去の記事で紹介していますので、そちらも参考にしてください。
自動生成の際にLiquibase用の設定ファイルが作成され、アプリケーション起動時にエンティティに対応したテーブルなどが作成されます。今回はそこにversion
カラムを作成するための設定を追加します。
src/main/resources/config/liquibase/changelog/
配下に20240424084327_added_entity_Employee_version.xml
を以下の内容で作成します。20240424084327
の部分についてはタイムスタンプなどの一意な値を使用します。今回は、自動生成されたエンティティのファイル名に合わせた値を使用しています。
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
<changeSet author="jhipster" id="20240424084327-1-add-version-column">
<addColumn tableName="employee">
<column name="version" type="bigint"/>
</addColumn>
</changeSet>
<changeSet id="20240424084327-1-version-faker" author="jhipster" context="faker">
<loadUpdateData
file="config/liquibase/fake-data/employee_version.csv"
separator=";"
onlyUpdate="true"
primaryKey="id"
tableName="employee">
<column name="id" type="numeric"/>
<column name="version" type="numeric"/>
</loadUpdateData>
</changeSet>
</databaseChangeLog>
employee
テーブルに対してversion
カラムを追加する設定と、ダミーデータとしてemployee_version.csv
を取り込む設定を記載しています。
src/main/resources/config/liquibase/master.xml
に対して、先ほど作成した設定ファイルを読み込むように<include file>
を追加します。
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<property name="now" value="now()" dbms="h2"/>
<property name="floatType" value="float4" dbms="h2"/>
<property name="uuidType" value="uuid" dbms="h2"/>
<property name="datetimeType" value="datetime(6)" dbms="h2"/>
<property name="clobType" value="longvarchar" dbms="h2"/>
<property name="blobType" value="blob" dbms="h2"/>
<property name="now" value="current_timestamp" dbms="postgresql"/>
<property name="floatType" value="float4" dbms="postgresql"/>
<property name="clobType" value="clob" dbms="postgresql"/>
<property name="blobType" value="blob" dbms="postgresql"/>
<property name="uuidType" value="uuid" dbms="postgresql"/>
<property name="datetimeType" value="datetime" dbms="postgresql"/>
<include file="config/liquibase/changelog/00000000000000_initial_schema.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20240424084327_added_entity_Employee.xml" relativeToChangelogFile="false"/>
<!-- jhipster-needle-liquibase-add-changelog - JHipster will add liquibase changelogs here -->
<!-- jhipster-needle-liquibase-add-constraints-changelog - JHipster will add liquibase constraints changelogs here -->
<include file="config/liquibase/changelog/20240424084327_added_entity_Employee_version.xml" relativeToChangelogFile="false"/>
<!-- jhipster-needle-liquibase-add-incremental-changelog - JHipster will add incremental liquibase changelogs here -->
</databaseChangeLog>
src/main/resources/config/liquibase/fake-data/
配下にemployee_version.csv
を作成し、version
カラムのダミーデータを記載します。
id;version
1;0
2;0
3;0
4;0
5;0
6;0
7;0
8;0
9;0
10;0
サーバ側のEntityへのカラム追加
次にsrc/main/java/com/mycompany/myapp/domain/Employee.java
に対して、追加したversion
カラムを扱うようにフィールドを追加します。
@Version
@Column(name = "version")
private Long version;
public Long getVersion() {
return this.version;
}
public Employee version(Long version) {
this.setVersion(version);
return this;
}
public void setVersion(Long version) {
this.version = version;
}
// jhipster-needle-entity-add-field - JHipster will add fields here
version
カラムにはJPAの@Version
アノテーションを設定し、楽観ロックとして動作するようにします。
フロントエンド側のEntityへのカラム追加
楽観ロックではversion
の値をフロントエンド側から受け渡す必要があるため、src/main/webapp/app/shared/model/employee.model.ts
に対して、version
を追加します。
export interface IEmployee {
id?: number;
firstName?: string | null;
lastName?: string | null;
version?: number | null;
}
version
の値を画面上に表示する必要はないため、その他のフロントエンド側のソースにversion
を追加する必要はありません。
なお、リレーションシップがあるテーブルに楽観ロックを適用したい場合は、Reducerでエンティティを取得する際に、関連するテーブルのversion
カラムを取得する処理を追加する必要があります。(本記事では詳細は割愛します)
論理削除
続けてソースコードに対してdelete_flag
カラムを追加して、レコードの論理削除を実現します。
LiquibaseによるDBへのカラム追加
Liquibaseの設定については、楽観ロックの場合とほぼ同様の内容です。delete_flag
カラムを作成するための設定を追加します。
src/main/resources/config/liquibase/changelog/
配下に20240424084327_added_entity_Employee_delete.xml
を以下の内容で作成します。ファイル名については楽観ロックの際と同様に、元エンティティのファイル名に合わせています。
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
<changeSet author="jhipster" id="20240424084327-1-add-delete-column">
<addColumn tableName="employee">
<column name="delete_flag" type="bigint"/>
</addColumn>
</changeSet>
<changeSet id="20240424084327-1-delete-faker" author="jhipster" context="faker">
<loadUpdateData
file="config/liquibase/fake-data/employee_delete.csv"
separator=";"
onlyUpdate="true"
primaryKey="id"
tableName="employee">
<column name="id" type="numeric"/>
<column name="delete_flag" type="numeric"/>
</loadUpdateData>
</changeSet>
</databaseChangeLog>
employee
テーブルに対してdelete_flag
カラムを追加する設定と、ダミーデータとしてemployee_delete.csv
を取り込む設定を記載しています。
src/main/resources/config/liquibase/master.xml
に対して、先ほど作成した設定ファイルを読み込むように<include file>
を追加します。
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<property name="now" value="now()" dbms="h2"/>
<property name="floatType" value="float4" dbms="h2"/>
<property name="uuidType" value="uuid" dbms="h2"/>
<property name="datetimeType" value="datetime(6)" dbms="h2"/>
<property name="clobType" value="longvarchar" dbms="h2"/>
<property name="blobType" value="blob" dbms="h2"/>
<property name="now" value="current_timestamp" dbms="postgresql"/>
<property name="floatType" value="float4" dbms="postgresql"/>
<property name="clobType" value="clob" dbms="postgresql"/>
<property name="blobType" value="blob" dbms="postgresql"/>
<property name="uuidType" value="uuid" dbms="postgresql"/>
<property name="datetimeType" value="datetime" dbms="postgresql"/>
<include file="config/liquibase/changelog/00000000000000_initial_schema.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20240424084327_added_entity_Employee.xml" relativeToChangelogFile="false"/>
<!-- jhipster-needle-liquibase-add-changelog - JHipster will add liquibase changelogs here -->
<!-- jhipster-needle-liquibase-add-constraints-changelog - JHipster will add liquibase constraints changelogs here -->
<include file="config/liquibase/changelog/20240424084327_added_entity_Employee_version.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20240424084327_added_entity_Employee_delete.xml" relativeToChangelogFile="false"/>
<!-- jhipster-needle-liquibase-add-incremental-changelog - JHipster will add incremental liquibase changelogs here -->
</databaseChangeLog>
src/main/resources/config/liquibase/fake-data/
配下にemployee_delete.csv
を作成し、delete_flag
カラムのダミーデータを記載します。
id;delete_flag
1;0
2;0
3;0
4;0
5;0
6;0
7;0
8;0
9;0
10;0
サーバ側のEntityへのカラム追加
src/main/java/com/mycompany/myapp/domain/Employee.java
に対して、追加したdelete_flag
カラムを扱うようにフィールドを追加します。
package com.mycompany.myapp.domain;
import jakarta.persistence.*;
import java.io.Serializable;
import org.hibernate.annotations.SQLRestriction;
/**
* A Employee.
*/
@Entity
@Table(name = "employee")
@SuppressWarnings("common-java:DuplicatedBlocks")
@SQLRestriction("delete_flag = 0")
public class Employee implements Serializable {
@Column(name = "delete_flag")
private Long deleteFlag = 0L;
public Long getDeleteFlag() {
return this.deleteFlag;
}
public Employee deleteFlag(Long deleteFlag) {
this.setDeleteFlag(deleteFlag);
return this;
}
public void setDeleteFlag(Long deleteFlag) {
this.deleteFlag = deleteFlag;
}
// jhipster-needle-entity-add-field - JHipster will add fields here
delete_flag
用のカラムを追加するまでは楽観ロック時と同様ですが、今回はエンティティのEmployee
クラスに対してSQLRestriction
アノテーションを設定し、論理削除されたレコードを除外しています。
Repositoryへの論理削除メソッド追加
src/main/java/com/mycompany/myapp/repository/EmployeeRepository.java
に対して、追加したdelete_flag
カラムを更新するためのメソッドを追加します。
package com.mycompany.myapp.repository;
import com.mycompany.myapp.domain.Employee;
import org.springframework.data.jpa.repository.*;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
/**
* Spring Data JPA repository for the Employee entity.
*/
@SuppressWarnings("unused")
@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long>, JpaSpecificationExecutor<Employee> {
@Modifying
@Query("update Employee set deleteFlag = 1 where id = :id")
void logicalDeleteById(@Param("id") long id);
}
Query
アノテーションでdelete_flag
を更新するSQL文を指定しています。
Repositoryに追加したメソッドはsrc/main/java/com/mycompany/myapp/service/EmployeeService.java
のdelete
メソッドでdeleteById
メソッドの代わりに使用します。
public void delete(Long id) {
log.debug("Request to delete Employee : {}", id);
employeeRepository.logicalDeleteById(id);
}
delete_flag
の値は画面上では使用しないため、フロントエンド側のソースに追加する必要はありません。
動作確認
動作確認のため./gradlew
でアプリケーションを起動します。
Administration
からDatabase
を選択し、H2Consoleを起動します。すると、employee
テーブルにversion
, delete_flag
カラムが追加されていることが確認できます。
楽観ロックの確認のため、Entities
からEmployee
を選択し、一覧画面から1つ目のデータの編集画面を2つ開きます。
それぞれFirst Name
のカラムを変更して、保存を行ってみます。
1つ目の編集画面での保存は成功し、2つ目の編集画面からの保存はサーバからConflict
エラーが返されました。
テーブル上のversion
カラムも更新されており、楽観ロックが機能していることが確認できます。
続いて論理削除の確認のため、Employee
の一覧画面から1つ目のデータを削除します。
一覧画面に1つ目のデータが表示されなくなっていることが確認できます。
テーブル上のdelete_flag
カラムが設定されており、論理削除が機能していることが確認できます。
このように自動生成されたコードに対してカラムを追加するだけで、論理削除や楽観ロックを実現できます。
Blueprintでのカラム追加
今回は1つのエンティティに対してカラム追加を実施しましたが、生成するエンティティの数が増えた場合は、個別に対応する手間が発生します。
この課題については、過去の記事で紹介したBlueprintを活用することで、自動生成の際にまとめてカラムを追加できます。
Blueprintとしては以下のような機能を実装することになります。
- JDL上にアノテーションとカラム名を付与してBlueprintで取得
- Liquibaseでカラムを追加するためのXML/CSVテンプレートを追加
- 追加したLiquibase設定ファイルを読み込むように
master.xml
に追加 Entity.java
に対して、フィールドとgetter/setter
を追加Entity.java
に対して、@SQLRestriction
アノテーションを追加(論理削除の場合)Repository.java
に対して、logicalDeleteById
メソッドを追加(論理削除の場合)Service.java
に対して、logicalDeleteById
メソッドの呼び出しを追加(論理削除の場合)model.ts
に対して、フィールドを追加(楽観ロックの場合)
JDL上のエンティティにアノテーションを付与する場合は以下のように記載します。
@version(version)
@deleteFlag(deleteFlag)
entity Employee {
FirstName String,
LastName String,
}
設定した値は、Blueprintからentity.annotations.version
などの変数で取得できます。
過去の記事で、テンプレートの追加や文字列の操作による要素の追加などを紹介していますので、具体的なやり方はそちらを参考にしてください。
また、Blueprintの少し特殊なテクニックとして、createNeedleCallback
関数を用いて要素を挿入する方法を紹介します。JHipsterが自動生成したソースコードにjhipster-needle-xxxxx-xxxxx
のようなコメントがあり、createNeedleCallback
を用いるとそのコメントの箇所に任意のコードを挿入できます。
以下はEntity.java
のjhipster-needle-entity-add-field
というコメント行に対して、@Version
フィールドを追加する例です。
get [BaseApplicationGenerator.POST_WRITING_ENTITIES]() {
return this.asPostWritingEntitiesTaskGroup({
async postWritingEntitiesTemplateTask({ entities }) {
for (const entity of entities.filter(entity => entity.annotations?.version)) {
this.editFile(
`src/main/java/${entity.entityAbsoluteFolder}/domain/${entity.persistClass}.java`,
createNeedleCallback({
needle: `entity-add-field`,
contentToAdd: ` @Version
@Column(name = "${entity.annotations?.version}")
private Long ${_.camelCase(entity.annotations?.version)};`,
}),
);
}
},
});
}
実際に追加されたフィールドは以下のようになります。
@Version
@Column(name = "version")
private Long version;
// jhipster-needle-entity-add-field - JHipster will add fields here
今回のケースではmaster.xml
へのXMLファイル追加や、Entity.java
へのフィールド、getter/setter
の追加の際に利用できます。
最後に
この記事では、JHipsterの自動生成結果に対して楽観ロックや論理削除に使用するカラムを追加する方法を解説しました。JHipsterを用いて開発する際の参考になれば幸いです。
JHipster 関連記事
- JHipster によるエンタープライズアプリケーションの構築(1) 紹介編
- JHipster によるエンタープライズアプリケーションの構築(2) Spring Native で高速起動化し AWS Lambda で動かす
- JHipster によるエンタープライズアプリケーションの構築(3) Blueprint でコード生成処理をカスタマイズする
- JHipster によるエンタープライズアプリケーションの構築(4) Liquibase によるデータベース管理
- JHipster によるエンタープライズアプリケーションの構築(5) Spring Modulith でモジュラーモノリス化する
- JHipster によるエンタープライズアプリケーションの構築(6) Blueprint でコード生成処理をカスタマイズする 2
- JHipster によるエンタープライズアプリケーションの構築(7) Amazon Cognitoを利用したユーザー認証