JavaのORM、Domaの話 +α

JavaのORM、Domaの話 +α

@backpaper0

2014-06-14 #uragamiorm

@backpaper0とは

  • うらがみです。
  • エスアイヤーでジャバ使ってウェッブやってます。
  • アンヨヨイヨも少しやってます。仕事で。
  • Doma歴4年ぐらい。

+αの話から

みなさんORM使っていますか?

backpaper0が知ってるORM

  • JPA (EclipseLink, Hibernate)
  • S2JDBC
  • S2Dao
  • ActiveObjects
  • Iciql (JaQu)
  • MyBatis (旧iBatis)
  • Mirage
  • Doma

JPA

  • Java EEに含まれるORMフレームワーク。
  • オブジェクトとリレーションの柔軟なマッピング。
  • JPQLでSQLの方言を吸収。
  • 生SQLも書ける。
  • Criteria API + Metamodel API = 型安全なクエリ。 ただしコード地獄になりがち。 ていうかなる。 クエリを再利用できるメリットある?
  • 少数精鋭向け?
  • QueryDSLを併用すれば幸せになれる?

JPAのエンティティ

@Entity
public class Book {

    @Id
    public String isbn;

    public String title;

    ...

JPAのクエリ

Book book = em.createQuery("SELECT b FROM Book b WHERE b.isbn = :isbn", Book.class)
              .setParameter("isbn", "978-4-488-10118-3")
              .getSingleResult();

Metamodel API

@StaticMetamodel(Book.class)
public class Book_ {

    public static SingularAttribute< Book, String> isbn;

    public static SingularAttribute< Book, String> title;

    public static SingularAttribute< Book, String> author;
}

Criteria API

CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery< Book> q = builder.createQuery(Book.class);
Root< Book> from = q.from(Book.class);
q.select(from);
q.where(builder.equal(from.get(Book_.isbn), "978-4-488-10118-3"));
Book book = em.createQuery(q).getSingleResult();

S2JDBC

  • Seasar2に含まれるORM。
  • JPAのアノテーションを利用する。
  • @ManyToMany には対応していない。
  • Namesクラスで型安全なクエリ。
  • 生SQLも書ける。
  • Seasar2を利用しない場合、設定(というかJdbcManagerインスタンスのセットアップ)がめんどい。
  • JTA必須っぽい。

S2JDBCのエンティティ

@Entity
public class Book {

    @Id
    public String isbn;

    public String title;

    ...

S2JDBCのクエリ

Book book = jdbcManager.from(Book.class)
                       .where(eq(isbn(), "978-4-488-10118-3"))
                       .getSingleResult();

S2Dao

  • Daoインターフェースを用意して実装は動的に生成する。
  • Seasar2必須と思う。
  • getBookByAuthorPublisherという風にメソッド名をもとにクエリを組み立てる。
  • SQLファイルを使用したりアノテーションにクエリ書いたりもできるっぽい。

ActiveObjects

  • エンティティはインターフェースでEntityインターフェースをextendsする。
  • アクセサっぽいメソッドを定義する。
  • ダイナミックプロキシで実装を生成している。
  • 2009年頃から更新されていないプロジェクトなので使ってはいけない

ActiveObjectsのエンティティ

public interface Book extends Entity {

    String getIsbn();

    void setIsbn(String isbn);

    ...

ActiveObjectsの永続化

net.java.ao.EntityManager em = ...
Book book = em.create(Book.class);
book.setIsbn("978-4-488-10118-3");
book.save();

Iciql

  • H2Databaseに付属のJaQuというORMが元になっている
  • 言葉では説明しづらい変わった方法でクエリを組み立てる

Iciqlのエンティティ

public class Book {

    @IQColumn(primaryKey = true)
    public String isbn;

    public String title;

    ...

Iciqlのクエリ

Book b = new Book();
Book book = db.from(b)
              .where(b.isbn).is("978-4-488-10118-3")
              .selectFirst();

Iciqlで結合

Book b = new Book();
Author a = new Author();
List< BookView> books = db.from(b)
        .innerJoin(a)
        .on(a.id).is(b.authorId)
        .select(new BookView() {{
            title = b.title;
            author = a.name;
        }});

Iciqlのもうひとつのクエリ

whereメソッドに渡したFilterの匿名サブクラスのバイトコードを解析してクエリを組み立てる。

List< Book> books = db.from(b).where(new Filter() {

    @Override
    public boolean where() {
        return b.isbn.equals("978-4-488-10118-3");
    }
}).select();

MyBatis

  • こざけさんが説明してくれるはず。

Mirage

  • GitBucket@takezoen さんが作成されているORM。
  • S2JDBCを手軽にした感じ?
  • でもタイプセーフクエリは無い。
  • mirage-scalaというのもある。

Doma

Domaとは

  • JavaのORM。
  • Object ResultSet Mapper (※個人の感想です)。
  • Pluggable Annotation Processing API を使用している。
  • その仕組み上、Scalaなど他の言語で書くことはできない。
  • 特定のアノテーションを付けたクラスやインターフェースをもとに補助クラスや実装クラスをコンパイル時にモリモリ生成

エンティティ

@Entity
public class Book {

    @Id
    public String isbn;

    public String title;

    public String author;

    ...

Daoインターフェース

@Dao(config = MyConfig.class)
public interface BookDao {

    @Select
    List< Book> select(String title, String author);

SQLファイル

META-INF/app/dao/BookDao/select.sql

SELECT /*%expand*/*
  FROM book
 WHERE title = /* title */'x'
   /*%if author != null */
   AND author = /* author */'y'
   /*%end*/

コンパイル時に色々検出

  • @Selectを付けたメソッドに対応するSQLファイルがないと コンパイルエラー
  • Daoクラスのメソッドに@Selectや@Insertなどのアノテーションが付いていないと コンパイルエラー
  • SQLファイルの中身が空っぽだと コンパイルエラー
  • メソッドの引数がSQLファイル内で使用されていないと コンパイルエラー
  • SQLファイル内の /*%if ...*//*%end*/ が変な位置にあると コンパイルエラー

ドメインクラス

エンティティのフィールドやDaoのメソッドの引数、戻り値にStringなどの基本型ではなくてユーザー定義のクラスを利用できる仕組み。

@Domain(valueType = String.class, factoryMethod = "of")
public class Isbn {

    private final String value;

    private Isbn(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }

    public static Isbn of(String value) {
        return Optional.ofNullable(value).map(Isbn::new).orElse(null);
    }
}

エンティティ + ドメインクラス

@Entity
public class Book {

    @Id
    public Isbn isbn;

    public Title title;

    public Author author;

    ...

Dao + ドメインクラス

@Dao(config = MyConfig.class)
public interface BookDao {

    @Select
    List< Book> select(Title title, Author author);

    @Select
    Title selectTitle(Isbn isbn);

SQLファイル + ドメインクラス

SQLファイルはドメインクラスを使用しない場合と何も変わらない。

SELECT /*%expand*/*
  FROM book
 WHERE title = /* title */'x'
   /*%if author != null */
   AND author = /* author */'y'
   /*%end*/

ドメインクラスの有無を比較

@Select
List< Book> select(Title title, Author author);

@Select
List< Book> select(String title, String author);

ドメインクラスを使用していると dao.select(author, title) はコンパイルエラーになる。

ジェネリックなドメインクラス

例えばサロゲートキーを表すドメインクラスがあったとする。

@Domain(valueType = Long.class)
public class SurrogateKey< T> {

    private final Long value;

    public SurrogateKey(Long value) {
        this.value = value;
    }

    public Long getValue() {
        return value;
    }
}

ジェネリックなドメインクラス

型変数にはエンティティをバインドする。

@Entity
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public SurrogateKey< Book> id;
@Entity
public class Author {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public SurrogateKey< Author> id;
author.id = book.id; //コンパイルエラー

StreamやCollectorへの対応

@Select
List< Book> select();

@Select(strategy = SelectType.STREAM)
< R> R select(Function< Stream< Book>, R> fn);

@Select(strategy = SelectType.COLLECT)
< R> R select(Collector< Book, ?, R> collector);
//タイトルをカンマ区切りで並べる
String titleList = select(s -> s.map(book -> book.title).collect(Collectors.joining(", ")));

Optionalへの対応

@Select
Optional< String> selectTitle(Isbn isbn);

@Select
String selectTitle(Optional< Isbn> isbn);

エンティティのフィールドにもOptionalは使える。

その他の機能

  • イミュータブルなエンティティ。
  • エンティティ作成の支援ツールdoma-gen。 テーブル定義からエンティティを生成する。
    • SQLファイルの実行結果からも生成できる? 試してない。
  • ローカルトランザクション。
  • 外部ドメイン。 既に存在しており変更できないクラスをドメインとして扱う。
  • クエリビルダ。 やむを得ず動的にSQLを組み立てるための補助的なクラス。
  • Date and Time API への対応。

公式ドキュメントなど

Doma http://doma.readthedocs.org/

( Doma 1.x http://doma.seasar.org/ )

作者: @nakamura_to さん

おわり。