美しき青きDoma!~SQLとIDEが奏でる美しきORマッピング~
このブログについて
これはJava Advent Calendar 2014 - Qiitaの10日目の記事です。
昨日(2014/12/09)は@irofさんの「Javaであまりしないコーディング - 日々常々」でした。
明日(2014/12/11)は@dk_masuさんです。
今回、僕は「Doma」というORマッパーについて調べました。
Domaには「バージョン 1」と「バージョン 2」がありますが、今回はJava SE 8に対応している「バージョン 2」を取り上げます。
Domaは日本語のチュートリアルが充実しているため、今回のブログでは、Domaを使うための最初の環境作成や、チュートリアルを読んで理解できるようになるための下地となる、基本的な部分について解説したいと思います。
Domaを使って、簡単な検索Webアプリを作ります。
サーブレット・JSPで作っていますので、適宜お使いのフレームワークに置き換えながらお読みください。
公式チュートリアルは下記です。
Welcome to Doma — Doma 2.0 ドキュメント
Javadocはこちら。
org.seasar.doma:doma:2.0.1 API Doc :: Javadoc.IO
このブログを読み終わったら、このチュートリアルを読んで、更に学習を進めてください。
僕はDomaを使うのは今回が初めてなので、勘違いしている部分などあるかもしれません。
何か気になる点などございましたら、コメントなどでご指摘ください。
今までのORマッパー遍歴
僕が今まで使ったことがある主なORマッパーは、S2JDBCとJPA(EclipseLink・Hibernate)なのですが、どちらも一長一短があると感じていました。
Domaに注目した理由
大規模システムでの事例あり!
最初のきっかけは、JJUG CCC 2014 Fallでの、@matsumanaさんのセッションでした。
テーブル数は約750、画面数は約1000という、かなり大規模なERPだそうです。
僕はちょうど同じ時間帯でラムダ式のセッションをしていたので聞けなかったのですが、実は一番聞きたかったセッションでした。
スライドは後ほど拝見しました↓
Spring Boot + Doma + AngularJSで作るERP #jjug_ccc #ccc_r12
ラムダ式、java.time、Optionalなど、Java SE 8の新機能に対応!
これはTwitterで@backpaper0さんから教えていただいて知りました。
(JPAはJava EE 8で対応するという噂)
依存ライブラリ、一切ナシ!
何かライブラリを使う際は、Mavenなどで依存関係を指定すると、芋ヅル式に大量の依存ライブラリがダウンロードされることが多いです。
そんなものは一切ナシで、DomaのJARファイルが1つあればOKです。
ですから、MVCやDIは他のフレームワーク使って、ORマッパーだけDoma、といったことも容易にできます。
例えば、上記の事例ではSpring Boot+Domaという組み合わせですね。
SQLは外部ファイルに書ける!
自分的には、これが一番デカいです。これは、S2Dao・S2JDBCなどのSeasar系ORマッパーの特徴を引き継いでいます。
select /*%expand*/* from emp where empno = /* empno */1
上記のようなSQLを、「1SQL 1ファイル」で書くことができます。
また、パラメータ変数は上記のようにコメントで指定し、なおかつダミー値を
「/* empno */1」
のようにコメント直後に書くことで、SQLファイル単体で実行することが可能になります。
プログラム内で文字列としてベタ書きすると、そんなことは出来ませんね(下記の様では・・・)。
String sql = new StringBuilder() .append("select ") .append("* ") .append("from ") .append("emp ") .append("where ") .append("empno = ? ") .toString();
で、連結する各文字列の最後に半角スペース入れ忘れて「select*fromemp・・・」とかなって実行時に例外(SQLのシンタックスエラー)が発生するとか、ありがちだと思います。
そういうことが無いのが非常にいいなと。
DomaでWebアプリを開発するための準備
長くなるので別記事にしました。
今回は、EclipseとGradleを使います。APサーバーはGlassFish 4.1(Tomcatでも大丈夫です)、DBはPostgreSQLです。
Doma 2をEclipse 4.4.1+Gradle 2.2で使うための環境設定・WTPプロジェクト作成・Doma利用設定まで - Java EE 事始め!
AppConfigクラスの作成
まず、JDBC設定やトランザクション管理の中心となる、AppConfigクラスを作成します。
package com.tada.doma2; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.sql.DataSource; import org.seasar.doma.SingletonConfig; import org.seasar.doma.jdbc.Config; import org.seasar.doma.jdbc.dialect.Dialect; import org.seasar.doma.jdbc.dialect.PostgresDialect; import org.seasar.doma.jdbc.tx.LocalTransactionDataSource; import org.seasar.doma.jdbc.tx.LocalTransactionManager; import org.seasar.doma.jdbc.tx.TransactionManager; @SingletonConfig public class AppConfig implements Config { // シングルトンインスタンス private static final AppConfig CONFIG = new AppConfig(); // SQL方言 private final Dialect dialect; // データソース private final LocalTransactionDataSource localTransactionDataSource; // トランザクション管理を行う private final TransactionManager transactionManager; private AppConfig() { try { dialect = new PostgresDialect(); DataSource dataSource = (DataSource) InitialContext.doLookup("jdbc/postgres"); localTransactionDataSource = new LocalTransactionDataSource(dataSource); transactionManager = new LocalTransactionManager( localTransactionDataSource.getLocalTransaction(getJdbcLogger())); } catch (NamingException e) { throw new RuntimeException(e); } } @Override public Dialect getDialect() { return dialect; } @Override public DataSource getDataSource() { return localTransactionDataSource; } @Override public TransactionManager getTransactionManager() { return transactionManager; } public static AppConfig singleton() { return CONFIG; } }
注意点としては、@SingletonConfigを付けた上で、シングルトンクラスとして作成することです。
また、シングルトンを返すメソッド名は「singleton()」と決められています。
Gradleタスクでエンティティ等を自動生成
genタスクの実行
build.gradleには、下記のようなタスクを記述します。
task gen << { ant.taskdef(resource: 'domagentask.properties', classpath: configurations.domaGenRuntime.asPath) ant.gen(url: 'jdbc:postgresql://localhost/postgres', user: 'postgres', password: 'P@ssw0rd') { entityConfig(packageName: 'com.tada.doma2.entity') daoConfig(packageName: 'com.tada.doma2.dao', configClassName: 'com.tada.doma2.AppConfig') sqlConfig() } }
このgenタスクを実行するには、プロジェクトを右クリック→[Gradle]-[タスク・クイック・ランチャー]です。
で、表示されたウィンドウに「gen」と記述し、Enterキーで実行です。
すると、エンティティ・リスナー・DAO・SQLファイルが自動生成されます。
DAO
package com.tada.doma2.dao; import com.tada.doma2.AppConfig; import com.tada.doma2.entity.Emp; import org.seasar.doma.Dao; import org.seasar.doma.Delete; import org.seasar.doma.Insert; import org.seasar.doma.Select; import org.seasar.doma.Update; @Dao(config = AppConfig.class) public interface EmpDao { @Select Emp selectById(Integer empno); @Insert int insert(Emp entity); @Update int update(Emp entity); @Delete int delete(Emp entity); }
インターフェイスであること、@Daoアノテーションが付加されconfig属性に先ほど作成したAppConfigが指定されていること、各メソッドには@Selectなどのアノテーションが付いていること、などが特徴です。
この○○Daoインターフェイスの実装クラス(○○DaoImpl)も自動生成されるのですが、ナビゲータービューからじゃないと見えません。
この○○DaoImplは、Java SE 6から導入された「Pluggable Annotation Processing API」で生成されています。
恥ずかしながら初耳でした・・・(^^;
エンティティ
package com.tada.doma2.entity; import java.time.LocalDate; import org.seasar.doma.Column; import org.seasar.doma.Entity; import org.seasar.doma.Id; import org.seasar.doma.Table; @Entity(listener = EmpListener.class) @Table(name = "emp") public class Emp { @Id @Column(name = "empno") Integer empno; @Column(name = "ename") String ename; @Column(name = "job") String job; @Column(name = "mgr") Integer mgr; // Date and Time API!! @Column(name = "hiredate") LocalDate hiredate; @Column(name = "sal") Integer sal; @Column(name = "comm") Integer comm; @Column(name = "deptno") Integer deptno; // 以下、setter/getter
日付がJava SE 8のLocalDateになっていますね!
Optionalを使う
EmpDaoクラスを少し書き換えます。
@Dao(config = AppConfig.class) public interface EmpDao { @Select Optional<Emp> selectById(Integer empno); ・・・
Java SE 8なので、Optionalを返すようにしてみました。
これで、nullチェックすることなく、検索結果の有無を判定できます。
主キー検索Webアプリを作る
サーブレット
package com.tada.doma2.servlet; import java.io.IOException; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.seasar.doma.jdbc.tx.TransactionManager; import com.tada.doma2.AppConfig; import com.tada.doma2.dao.EmpDao; import com.tada.doma2.dao.EmpDaoImpl; import com.tada.doma2.entity.Emp; @WebServlet("/EmpnoSelectServlet") public class EmpnoSelectServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int empno = Integer.parseInt(request.getParameter("empno")); // 本来はDIで取得 EmpDao empDao = new EmpDaoImpl(); TransactionManager tm = AppConfig.singleton().getTransactionManager(); Emp emp = tm.required(() -> { return empDao.selectById(empno); }).orElseGet(() -> { Emp other = new Emp(); other.setEmpno(empno); other.setEname("存在しません"); return other; }); request.setAttribute("emp", emp); RequestDispatcher rd = request.getRequestDispatcher("/empnoSelectResult.jsp"); rd.forward(request, response); } }
検索の際には、必ずトランザクションが開始されていなければなりません。
それが
tm.required(() -> {
・・・
})
の部分です。
このラムダ式内がトランザクション内となります。
処理が正常に完了した際はコミットされ、例外が発生した場合はロールバックされます。
このラムダ式はSupplierで、ラムダ式内でreturnされた値が、そのままrequired()メソッドの戻り値となります。
社員番号入力画面(empnoSelectIndex.jsp)
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <title>社員番号入力画面</title> </head> <body> <form action="./EmpnoSelectServlet"> <input type="text" name="empno"> <input type="submit" value="送信"> </form> </body> </html>
結果表示画面(empnoSelectResult.jsp)
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <title>結果表示画面</title> </head> <body> ${emp.empno}の社員は${emp.ename}です。<br> <a href="./empnoSelectIndex.jsp">社員番号入力画面へ</a> </body> </html>
実行結果
表の結合
Domaでは、JPAのように1対nのリレーションシップはありません。
例えば、EmpとDeptを結合したい場合は、新たにエンティティを作ることになります。
FAQ — Doma 2.0 ドキュメント
エンティティ
package com.tada.doma2.entity; import org.seasar.doma.Entity; import org.seasar.doma.Id; @Entity public class EmpJoinDept { @Id Integer empno; String ename; Integer deptno; String dname; // 以下、setter/getter }
DAOとSQLファイル
EmpDaoにメソッドを追加します。
public interface EmpDao { ・・・ // メソッド追加 @Select Optional<EmpJoinDept> selectByIdJoinDept(Integer empno); ・・・ }
作ったメソッドには、コンパイルエラーが出ていると思います。
これは、このメソッドに対応するSQLファイルが、まだ無いからです。
では、このメソッドを1回左クリックしてカーソルを当ててから、右クリック→[Doma]-[Jump to Sql File]とします。
そうすると、「(メソッド名).sql」というファイルが作成されます。
このファイルに、SQLを記述します。
ちなみに、SQLはまずpgAdminなどのDB管理ツール上で書いてから、SQLファイルにコピペするという方法が推奨されます。
SQLファイルが作成されたら、DAOのコンパイルエラーは消えます。
サーブレット
package com.tada.doma2.servlet; import java.io.IOException; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.seasar.doma.jdbc.tx.TransactionManager; import com.tada.doma2.AppConfig; import com.tada.doma2.dao.EmpDao; import com.tada.doma2.dao.EmpDaoImpl; import com.tada.doma2.entity.EmpJoinDept; @WebServlet("/EmpnoSelectJoinDeptServlet") public class EmpnoSelectJoinDeptServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int empno = Integer.parseInt(request.getParameter("empno")); EmpDao empDao = new EmpDaoImpl(); TransactionManager tm = AppConfig.singleton().getTransactionManager(); EmpJoinDept emp = tm.required(() -> { return empDao.selectByIdJoinDept(empno); }).orElseGet(() -> { EmpJoinDept other = new EmpJoinDept(); other.setEname("存在しません"); return other; }); request.setAttribute("emp", emp); RequestDispatcher rd = request.getRequestDispatcher("/empnoSelectJoinDeptResult.jsp"); rd.forward(request, response); } }
社員番号入力画面(empnoSelectJoinDeptIndex.jsp)
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <title>社員番号入力画面(結合版)</title> </head> <body> <form action="./EmpnoSelectJoinDeptServlet"> <input type="text" name="empno"> <input type="submit" value="送信"> </form> </body> </html>
結果表示画面(empnoSelectJoinDeptResult.jsp)
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <title>結果表示画面</title> </head> <body> 社員番号:${emp.empno}<br> 社員名:${emp.ename}<br> 部署番号:${emp.deptno}<br> 部署番号:${emp.dname}<br> <a href="./empnoSelectJoinDeptIndex.jsp">社員番号入力画面へ</a> </body> </html>
実行結果
今回調べきれなかったこと
長くなってきたので、今回のブログはここまでにしたいと思います。
今回は書けなかった・調べきれなかった主な事項を簡単に列挙します。
ドメイン
Domaのもう1つの特徴として、ドメインクラスがあります。
これは、テーブルの列の型(=エンティティのフィールドの型)として定義するものです。
ドメインを使わない場合、
@Select public List<Emp> selectByEnameAndJob(String ename, String job); // ① List<Emp> list = empDao.selectByEnameAndJob("Ni", "Director"); // ② List<Emp> list = empDao.selectByEnameAndJob("Director", "Ni");
①と②は、どちらもコンパイルが通ってしまいます。
そこで下記のようにドメインを使えば、それを防ぐことができます。
@Select public List<Emp> selectByEnameAndJob(Ename ename, Job job);
これは重要な機能なので、今後さらに調査したいと思います。
JTAでのトランザクション管理
今回はDomaのLocalTransactionを利用しましたが、グローバルトランザクションを利用したい場合は、JTAを使わなければなりません。
JTAを使う場合にどう書けばいいのか、今回は分かりませんでした。
おそらくAppConfigクラスの書き方をどうにか変えた上で、こんな感じで書けると思うのですが・・・。
@Transactional(TxType.REQUIRED) @RequestScoped public class EmpService { @Inject EmpDao empDao; public Optional<Emp> findById(Integer empno) { return empDao.selectById(empno); } }
DAOをDIする方法
上記のコードのように、EmpDaoをDIする場合、CDIだと「bean-discovery-mode="all"」かなあ・・・。
でも何でもかんでもDIできるようになっちゃうので、危険な香りがするなあ・・・。
あとSpringにも、これに相当する設定があるんでしょうか?
まとめ
Doma 2はシンプル・イズ・ベストだと感じました。
複雑なものはいっさい省き、SQLをエンティティにマッピングするというところに集中しています。
今回は簡単なサンプルしか作っていませんが、変な「ハマりどころ」が無いと思います。
ただ、エンティティを自動生成する際に、対象スキーマ内の全テーブルのエンティティを、1つのパッケージに生成してしまうので、テーブル数が多いと管理が大変そうです。
genタスク実行の際に、対象テーブルを絞ったり、パッケージを分割したりすることは出来るのでしょうか。
それも要研究ですね。
雑感
まだまだ自分の非力さを感じました。技術的にも、文章的にも。
「美しき」をうまく表現できなかったなあ、というのが実感です。反省。
Advent CalendarもDomaも初めてだったので、どうかご勘弁ください。
ここまでお読みくださって、ありがとうございました!
ちなみに
「青き」はSeasarのイメージカラーから来ています。
The Seasar Project