JHipster によるエンタープライズアプリケーションの構築(8) 楽観ロックと論理削除

カバー

システム開発において、楽観ロックや論理削除を用いることがよくあります。

しかし、JHipsterでは楽観ロックや論理削除の実装は自動生成することができません。同じデータに対する複数の更新が競合した際には最後の更新で保存され、データの削除は物理的な削除となります。

この記事では、JHipsterの自動生成後に追加のカラムと実装を加え、楽観ロックや論理削除を行う手法を紹介します。

この記事では、JHipsterのバージョン8.4.0を使用しています。

ベースプロジェクトの作成

楽観ロックや論理削除を追加するため、ベースとなるJHipsterのプロジェクトを作成します。

Terminal window
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ファイルを指定してアプリケーションを生成します。

Terminal window
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.javadeleteメソッドで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.javajhipster-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 関連記事

参考文献


TOP
アルファロゴ 株式会社アルファシステムズは、ITサービス事業を展開しています。このブログでは、技術的な取り組みを紹介しています。X(旧Twitter)で更新通知をしています。