Java
ORM
doma2
永続化

2017年度 Java 永続化フレームワークについての考察(2) Doma2

前ポスト

前置き

隙間時間を利用して書いているので精度の甘い箇所があれば、ご指摘いただけると幸いです。
まずEOLであるものは除外。
有償の機能については検討対象に含むが、実際に使用はしません、懐の問題で。
「まぁ、たぶん明記されてなくてもPostgreならいけるだろ」ぐらいの間抜けな理由で使用DBはPostgreに固定。

環境

  • PostgreSQL 9.6.3
  • Doma2.16.1
  • JUnit4.12
  • Java 64bit 8u143 (※Java9+IntelliJだと注釈機能が機能しないことを確認、モジュール化の影響?)
  • なんかmavenもgitも繋がらないので手動DL、もろもろ直るかも

テーブル構成

table_1.png

対応範囲

ORM Transaction Data Model DSL
× ×

○:対応
×:非対応

所感

  1. 注釈処理 is 強い
  2. 深入りすると大分知識が要求されるイメージ、Doma2専門家1人に複数名の開発者をぶら下げる構造で利用したい
  3. ことごとく実行前にダメ出しするスタイル、注釈処理 is 強い
  4. SQLが外部化されているので、SQL単品でのテストや実行計画の取得が可能

サンプル

単テーブル検索

主キー検索

SelectByIdTest.java
EmployeeDao dao = new EmployeeDaoImpl();
AppConfig.singleton().getTransactionManager().required(() -> {
    Employee e1 = dao.selectById(BigDecimal.ZERO);
});

Stream検索

SelectByFirstNameAsStreamTest.java
EmployeeDao dao = new EmployeeDaoImpl();
AppConfig.singleton().getTransactionManager().required(() -> {
    // EmployeeDao#selectByFirstNameAsStream(String)は手動で追加したシグニチャ
    Employee e1 = dao.selectByFirstNameAsStream("John");
});
EmployeeDao.java
// 引数名とSQLファイルで使用するパラメータは同名にする必要がある
@Select
Stream<Employee> selectByFirstNameAsStream(String first_name);
META-INF/~/EmployeeDao/selectByFirstNameAsStream.sql
select
  /*%expand*/*
from
  employee
where
  first_name = /* first_name */1

コールバック風

SelectByFirstNameForCountTest.java
EmployeeDao dao = new EmployeeDaoImpl();
AppConfig.singleton().getTransactionManager().required(() -> {
    System.out.println(
        // EmployeeDao#selectByFirstNameForCount(String, Function<Stream<Employee>, AtomicInteger>)は手動で追加したシグニチャ
        dao.selectByFirstNameForCount(
            "太郎",
            e -> new AtomicInteger((int)e.count())));
});
EmployeeDao.java
// IntFunctionを受け付けてくれないとは思わなかった
@Select(strategy = SelectType.STREAM)
AtomicInteger selectByFirstNameForCount(String first_name, Function<Stream<Employee>, AtomicInteger> mapper);
META-INF/~/EmployeeDao/selectByFirstNameForCountTest.sql
select
  /*%expand*/*
from
  employee
where
  first_name = /* first_name */1

テーブル結合

EmployeeWithPost.java
@Entity(naming = NamingType.SNAKE_LOWER_CASE)
public class EmployeeWithPost {
    /** */
    @Id
    @Column(name = "id")
    BigDecimal id;

    /** */
    @Column(name = "first_name")
    String firstName;

    /** */
    @Column(name = "middle_name")
    String middleName;

    /** */
    @Column(name = "last_name")
    String lastName;

    /** */
    @Id
    @Column(name = "post_id")
    BigDecimal postId;

    /** */
    @Column(name = "name")
    String name;

    public void setId(BigDecimal id) {
        this.id = id;
    }

    public BigDecimal getId() {
        return this.id;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getFirstName() {
        return this.firstName;
    }

    public void setMiddleName(String middleName) {
        this.middleName = middleName;
    }

    public String getMiddleName() {
        return this.middleName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getLastName() {
        return this.lastName;
    }

    public void setPostId(BigDecimal postId) {
        this.postId = postId;
    }

    public BigDecimal getPostId() {
        return this.postId;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }

    public String toString() {
        return "EmployeeWithPost["
                + this.id
                + ":"
                + this.firstName.trim()
                + "/"
                + this.middleName.trim()
                + "/"
                + this.lastName.trim()
                + " "
                + this.postId
                + "-"
                + this.name.trim()
                + "]";
    }
}
EmployeeWithPostDao.java
@Dao(config = ORMConfig.class)
public interface EmployeeWithPostDao {
    @Select
    public List<EmployeeWithPost> selectByEmployeeId(BigDecimal employeeId);
}
META-INF/(パッケージ名)/EmployeeWithPostDao/selectByEmployeeId.sql
select
  employee.id AS id,
  employee.first_name AS first_name,
  employee.middle_name AS middle_name,
  employee.last_name AS last_name,
  post.id AS post_id,
  post.name AS name
from
  employee
  INNER JOIN
    post
  ON
    post.employee_id = employee.id
where
  employee_id = /* employeeId */1

要点

  1. カバー範囲はORM+DBアクセスライブラリ+トランザクション(部分的)
    • トランザクションはローカルトランザクションのみをサポート
    • グローバルトランザクションを使用したい場合にはJTA実装のライブラリを導入する必要がある
  2. 注釈処理により、SQLとDAOのマッピング不備はコンパイル時点で発覚する
    • SQLには独自の式言語が使用可能だが、その妥当性検証もコンパイル時点で行われる
  3. Entityの自動生成あり
    • SQLファイルも主キー検索は自動生成される
  4. テーブル結合結果を格納する場合、カスタムEntityとして自作
    • 同様にDAOも自作するのだが、DAOはInterfaceであり、紐づいたSQLファイルから実装クラスが自動生成される
  5. DAOとSQLのマッピングはメソッド名→ファイル名が1:1の命名規則による規約
  6. 検索結果に対するキャッシュあり、生存期間はトランザクション中
  7. 検索結果をjava.util.stream.Streamで返すことが可能
    • java.util.stream.Streamを受け渡しする場合にはライフサイクルに注意が必要であることを留意
    • もっとも、Doma2ではその点についてWarningを出してくれる

ハマったポイント

  1. いきなりサンプルを無視、ソース生成先からクラスを取得できずに無事死亡
  2. AppConfig(※コンフィグクラス)もサンプルを無視、生成されるコードに差異が現れ無事死亡
    • @Singletonの有無で、自動生成されるDaoImpl引数なしコンストラクタの挙動が異なる
      1. @Singleton有の場合、AppConfig.singleton()にて取得したインスタンスを使用
      2. @Singleton無の場合、new AppConfig()にて取得したインスタンスを使用
    • サンプルは@Singleton有、作成したのは@Singleton無、テストコードはサンプル通り、結果動かなかった
    • 動かなかった理由はAppConfig内でTransactionManagerを管理しているため
      1. @Singleton無のコードで、@Singleton有のテストコードを使用すると、前述の仕様からDAOとAppConfigが別トランザクションとなる
      2. Doma2ではTransactionManagerのメソッド内でトランザクションを管理する
      3. DAO産とAppConfig産のトランザクションが発生
      4. AppConfig産のトランザクション内でDAO産のトランザクションを操作しようとする
  3. ソースディレクトリ直下に「META-INF/(パッケージ名)」の構造を作り、SQLファイルを配置する
    • プロジェクトや会社によっては集計用ツールなどで使えない可能性がある
    • 配置先を変更できるかどうかは未確認

後ポスト

参考記事