Entity・Repository 実装ガイド

1. はじめに

(1) パッケージ・クラス概要

(ア) domain.entityパッケージ(Entityクラス、Viewクラス)

ビジネスロジックにおける登録・更新対象のデータや、検索結果を格納するためのクラスを提供します。
Entityはテーブル、ビューと1:1に対応するクラスです。
Viewは、複雑なクエリの検索結果を格納するクラスです。

(イ) domain.repositoryパッケージ(Repositoryインターフェース、Queriesインターフェース)

Entityの永続化やViewの検索のためのIFを提供します。
ビジネスロジックと、データベース操作を分離することが目的です。
Entityの保存や検索には、Repositoryを使用します。
Viewの検索には、Queriesを使用します。

(ウ)adapter.repositoryパッケージ(RepositoryImplクラス、QueriesImplクラス)

Repository、Queriesの実装を提供します。
RepositoryImplでは、Panache(Quarkus)を使用し、データベース操作を行います。
QueriesImplでは、Mapper(MyBatis)を使用し、データベース操作を行います。

(2) データベース操作の実装方針

データベース操作にはJPAまたは、MyBatisを使用します。
各操作方法の特徴と使い分けの基準について説明します。

(ア) JPA(Java Persistence API)の特徴

JPAでは、テーブルと1:1に対応したエンティティを使用して、データベースを操作します。
JPAが提供するメソッドを利用することで、クエリの自動生成や、トランザクション管理が容易に行えます。
単一テーブル、親子関係のあるのテーブルに対する単純なCRUD操作や、JOINを伴わないクエリ操作に適しています。

(イ) MyBatisの特徴

MyBatisは、SQLを直接記述することでデータベースを操作します。
SQLの記述が自由度が高く、複雑なクエリ操作や、パフォーマンスチューニングが容易に行えます。

(ウ) 使い分けの基準

単一テーブル、親子関係のあるのテーブルに対する単純なCRUD操作や、JOINを伴わないクエリ操作にはJPAを使用し、
その他の操作にはMyBatisを使用することを推奨します。

2. データベース操作

JPA、MyBatisを利用したデータベース操作の実装方法を記載します。 JPAは、対象のテーブルが単一である場合と、親子関係がある場合の実装例を示します。

(1) JPAを利用したデータベース操作(単一のテーブル)

(ア)Entityの実装

JPAで使用するエンティティは、テーブルと 1:1 に対応します。
対象となるテーブル名の末尾に「Entity」と付与したものをクラス名としてください。
「srcmainjavaogis_ridx_support【業務領域名】domainentity」フォルダに作成します。
以下に、商品テーブル定義と対応するエンティティの実装例を示します。

Note

・Entityは、今後 DDL 定義からひな形の自動生成を予定しています。
../_images/model_Products.png ../_images/ddl_Products.png
package ogis_ri.dx_support.orders.domain.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity                                         // @Entityを必ず付与する
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "products", schema = "orders")    // テーブル名、スキーマ名の指定
public class Products {

    @Id                                         // 主キー項目には@Idアノテーションを付与
    @Column(name = "product_id")                // 対応するカラム名の指定
    String productId;

    @Column(name = "product_name")
    String productName;

    @Column(name = "costPrice")
    Integer costPrice;

}

(イ)Repository、RepositoryImplの実装

Repository、RepositoryImplはエンティティと1:1に対応します。
対象となるエンティティ名の末尾に「Repository」または「RepositoryImpl」と付与したものをクラス名としてください。
以下はRepositoryクラスの実装例になります。
「srcmainjavaogis_ridx_support【業務領域名】domainrepository」フォルダに作成します。
package ogis_ri.dx_support.orders.domain.repository;

import java.util.Optional;

import ogis_ri.dx_support.orders.domain.common.QueryBase;
import ogis_ri.dx_support.orders.domain.common.QueryParam;
import ogis_ri.dx_support.orders.domain.common.QueryResult;
import ogis_ri.dx_support.orders.domain.entity.ProductEntity;

public interface ProductsRepository {

    // 登録
    void save(ProductEntity ent);

    // Id検索
    Optional<ProductEntity> get(String productId);

    // クエリ検索
    QueryResult<ProductEntity> query(QueryBase queryBase, QueryParam queryParam);

    // 更新
    void update(ProductEntity ent);

    // 削除
    boolean delete(String productId);

}
以下はRepositoryImplクラスの実装例になります。
「srcmainjavaogis_ridx_support【業務領域名】adapterrepository」フォルダに作成します。
package ogis_ri.dx_support.orders.adapter.repository;

import java.util.Optional;

import io.quarkus.hibernate.orm.panache.PanacheQuery;
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
import io.quarkus.panache.common.Sort;
import jakarta.enterprise.context.Dependent;
import ogis_ri.dx_support.orders.domain.common.QueryBase;
import ogis_ri.dx_support.orders.domain.common.QueryParam;
import ogis_ri.dx_support.orders.domain.common.QueryResult;
import ogis_ri.dx_support.orders.domain.entity.ProductEntity;
import ogis_ri.dx_support.orders.domain.repository.ProductsRepository;

@Dependent
public class ProductsRepositoryImpl
        implements ProductsRepository,
        PanacheRepositoryBase<ProductEntity, String> { // PanacheRepositoryBaseを継承し、対象のEntityと主キーの型を指定

    @Override
    public void save(ProductEntity ent) {
        // 以下のメソッドにより、テーブルにEntityの内容を登録します。
        return persist(productId);
    }

    @Override
    public Optional<ProductEntity> get(String productId) {
        // 以下のメソッドにより、テーブルから指定した主キーのEntityを取得します。
        // 対象が存在しない場合は、Optional.empty()が返却されます。
        return findByIdOptional(orderNo);
    }

    // 下記のクエリ検索については後述します。
    @Override
    public QueryResult<ProductEntity> query(QueryBase queryBase, QueryParam queryParam) {
        PanacheQuery<ProductEntity> pq;
        Sort sort = Sort.by("productId"); // ソート順の指定

        if (queryParam.isEmpty()) {
            pq = findAll(sort);
        } else {
            pq = find(queryParam.buildQuery(), sort, queryParam.getParameters());
        }

        return QueryResult.<ProductEntity>builder()
                .resultList(pq.range(queryBase.getFromNum(), queryBase.getToNum()).list())
                .isRemaining(queryBase.isRemaining(pq.count()))
                .build();
    }

    @Override
    public void update(ProductEntity ent) {
        // 更新を行う場合は、まず上記のgetメソッドなどで更新対象のEntityを取得し、 内容を編集します。
        // JPAの仕様上、取得したEntityを編集した段階で、トランザクション終了時に自動的に更新処理が行われますが、
        // 更新処理を行うことを明示的に示すために、以下のメソッドを呼び出してください。
        // 上記の仕様については、公式ドキュメントを参照してください。
        persist(ent);
    }

    @Override
    public boolean delete(String productId) {
        // 以下のメソッドにより、テーブルから指定した主キーのレコードを削除します。
        // 対象が存在しない場合は、falseが返却されます。
        return deleteById(productId);
    }

}

(ウ) クエリ検索について

JPAでのクエリ検索の為、本プロジェクトでは以下の3クラスを提供しています。
Panache(Quarkus)が提供するクラスでないことに注意してください。
これらのクラスを利用することで、柔軟にクエリを構築することができますが、
内容が複雑になる場合はJPAの利用は避け、MyBatisを使用することを検討してください。
  • QueryBase

検索結果数上限値、OFFSET値など、汎用的な検索条件を提供するクラスです。
以下は使用例になります。
QueryBase queryBase = QueryBase.builder()
        .limit(20) // 検索結果数上限値
        .offset(0) // OFFSET値
        .build();
  • QueryParam

テーブル固有のクエリを構築するためのクラスです。
以下は使用例になります。
詳細なパラメータの指定方法は、実装を参照してください。
QueryParam queryParam = new QueryParam()
            .add("productId", EQUAL, "ABC000000")
            .add("costPrice", LESS, "3000");
  • QueryResult

クエリ検索の検索結果を格納するクラスです。
Entityのリストと、検索結果上限値を超えるデータが存在した場合trueとなる真偽値を保持します。

(2) JPAを利用したデータベース操作(親子関係のテーブル)

(ア)Entityの実装

以下に受注、受注明細のテーブル定義と、対応するEntityの実装例を示します。
../_images/model_Receivings.png
../_images/ddl_Receivings.png ../_images/ddl_ReceivingDetails.png
以下は受注のEntityクラスの実装例になります。
package ogis_ri.dx_support.orders.domain.entity;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity                                         // @Entityを必ず付与する
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "receivings", schema = "orders")  // テーブル名、スキーマ名の指定
public class ReceivingEntity {

    @Id                                         // 主キー項目には@Idアノテーションを付与
    @Column(name = "order_no")                  // 対応するカラム名の指定
    String orderNo;

    @Column(name = "order_date")
    LocalDate orderDate;

    @Column(name = "operator_name")
    String operatorName;

    @Column(name = "customer_name")
    String customerName;

    @Column(name = "total_order_price")
    Integer totalOrderPrice;

    @Column(name = "remaining_order_price")
    Integer remainingOrderPrice;

    @Column(name = "order_status")
    @Enumerated(EnumType.STRING)                // Enumを文字列形式で登録するための設定
    OrderStatusTypeEnum orderStatus;

    // 親子で 1:n の関係を持つ場合、親のEntityに@OneToManyを付与(※)
    // mappedByには、子のEntityに設定した親のEntityのフィールド名を指定
    @OneToMany(mappedBy = "receiving", cascade = CascadeType.ALL)
    @Builder.Default
    private List<ReceivingDetailEntity> receivingDetails = new ArrayList<>(); // 子のEntityをリストで保持

    // (※親子の関係が 1:1 の場合は、@OneToOneを使用し、子のEntityはリストではなくフィールドで保持してください)

    // 受注の主キーと受注明細の外部キーを一致させ、親子のEnitity同士の関連付けを行う。
    public void addReceivingDetail(ReceivingDetailEntity receivingDetail) {
        receivingDetail.setOrderNo(this.orderNo);
        receivingDetail.setReceiving(this);
        receivingDetails.add(receivingDetail);
    }
}
以下は受注ステータスを表すEnumクラスの実装例になります。
package ogis_ri.dx_support.orders.domain.entity;

public enum OrderStatusTypeEnum {

    WORK_IN_PROGRESS, // 仕掛かり
    CANCELED, // キャンセル
    COMPLETED, // 出荷完了
    PREPARING; // 準備中

}
以下は受注明細のEntityクラスの実装例になります。
受注明細は複合キーを持つため、IdClassを付与して複合キーを定義します。
package ogis_ri.dx_support.orders.domain.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@IdClass(ReceivingDetailEntityPK.class)     // 複合キーを持つ場合、@IdClassを付与
@Table(name = "receiving_details", schema = "orders")
public class ReceivingDetailEntity {

    @Id
    @Column(name = "order_no")
    String orderNo;

    @Id
    @Column(name = "product_id")
    String productId;

    @Column(name = "receiving_quantity")
    Integer receivingQuantity;

    @Column(name = "shipping_quantity")
    Integer shippingQuantity;

    @Column(name = "cancel_quantity")
    Integer cancelQuantity;

    @Column(name = "remaining_quantity")
    Integer remainingQuantity;

    @Column(name = "sellling_price")
    Integer selllingPrice;

    @Column(name = "cost_price")
    Integer costPrice;

    @Column(name = "profit_rate")
    Double profitRate;

    @Column(name = "order_status")
    OrderStatusTypeEnum orderStatus;

    // 親子で 1:n の関係を持つ場合、子のEntityに@ManyToOneを付与(※)
    @ManyToOne
    @JoinColumn(name = "order_no", insertable = false, updatable = false) //nameには、外部キーとなる項目名を指定
    private ReceivingEntity receiving;

    // (※親子の関係が 1:1 の場合は、@OneToOneを使用してください)
}
package ogis_ri.dx_support.orders.domain.entity;

import lombok.EqualsAndHashCode;

@EqualsAndHashCode // 必ず付与する
public class ReceivingDetailEntityPK implements Serializable { // Serializableを実装

    // 受注明細Entityで@Idアノテーションを付与した項目と一致させる必要がある
    String orderNo;
    String productId;

}

Caution

・JPAを使用する場合、保守性の観点から複合キーの使用は避けることを推奨します。

(イ)Repository、RepositoryImplの実装

単一テーブルの場合と同様の為、実装例は省略します。
親子関係のあるエンティティの場合、親エンティティに対して行ったデータベース操作は、子エンティティにも反映されます。
今回の例では、受注エンティティに対してCRUD処理を行うと、紐づいた受注明細エンティティにも処理が伝播します。
処理の伝播可否については、@OneToMany、@ManyToOneの属性で設定可能です。
詳細は公式ドキュメントを参照してください。

(3) MyBatisを利用したデータベース操作

(ア)Viewの実装

特別な設定は不要ですので、実装例は省略します。
クエリ検索結果として必要な項目を持つクラスを作成してください。
テーブルとの密な対応関係はありません。任意の名称の末尾に「View」と付与したものをクラス名としてください。
「srcmainjavaogis_ridx_support【業務領域名】domainentity」フォルダに作成します。

(イ)Queries、Mapperの実装

テーブルやエンティティとの密な対応関係はありません。
任意の名称の末尾に「Queries」または「Mapper」と付与したものをクラス名としてください。
操作の基準となるテーブルごとにクラスを作成することを推奨します。
以下はQueries、Mapperの実装例になります。
「srcmainjavaogis_ridx_support【業務領域名】adapterrepository」フォルダに作成します。
package example_corp.example_project.pets.adapter.repository;

import java.util.List;

import example_corp.example_project.pets.domain.entity.PetsAndTagView;
import example_corp.example_project.pets.domain.repository.PetsQueries;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

@ApplicationScoped
public class PetsQueriesImpl implements PetsQueries {

    @Inject
    PetsMapper mapper; // 使用するmapperをインジェクト

    @Override
    @SuppressWarnings("unchecked")
    public List<PetsAndTagView> listPetsByTags(List<String> tags) {
        // mapperに定義しているSQLを呼び出す
        return mapper.listByTagNames(tags);
    }
package example_corp.example_project.pets.adapter.repository;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import example_corp.example_project.pets.domain.entity.PetsAndTagView;

/**
* MyBatisのMapperのサンプル.
*
*/

@Mapper
public interface PetsMapper {

    // MyBatisのアノテーションを使用してSQLを記述します。
    // 全体をscriptタグで囲むことで、動的なクエリをXML記法で記述することができます。
    @Select("""
            <script>
                select
                    p.id as id
                    , p.name as pets_name // エイリアスと、Viewのフィールド名を一致させることでマッピングを行います。
                    , pt.name as tag_name
                from
                    pets.pets p
                join pets.pets_tag pt
                    on pt.id = p.id
                join pets.tags t
                    on t.name = pt.name
                where
                    pt.name in
                        <foreach item="tag" collection="tag_names" open="(" separator="," close=")">
                            #{tag}
                        </foreach>
            </script>
                """)
    List<PetsAndTagView> listByTagNames(@Param("tag_names") List<String> tagNames);

}

.. caution::

    | もう一個ifを使った場合の実装例を記載する