ちょっと話題の記事

Mirage SQL 〜 2WaySQLをつかうデータアクセスライブラリ for Java

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

よく訓練されたアップル信者、都元です。Spring連載はもうちっとお待ちください。今回はその布石ということで。

DBアクセスというのは、システムを作る上で大抵避けられない領域でしょう。まぁ近年はRDBMSに限らず、各種NOSQLの台頭が目覚ましいわけですが、いまだRDBMSの世界は多く残っています。

そこで。今、Javaでサーバサイドアプリケーションを書くことになった時、データアクセスのフレームワークには何を使いましょうか。Hibernate (JPA)でしょうか。MyBatisでしょうか。Domaでしょうか。どれも凄いフレームワークです。どれを選ぶのか、真っ当な結論から先に言ってしまえば、そんなの案件次第なわけですがw

そんな中、個人的に非常に使い勝手が良いと評価しているフレームワークに「Mirage SQL」があります。今回は、このMirageについてご紹介します。

Mirage SQL

サイトを見るとドキュメントが全て英語なのですが、実は作者の方は日本人です。気に入って使っているうちに、色々要望が出てきてしまい、最初はメールで相談していたのですが、もうコミットしてしまえということで、現在は私自身も開発に参加させて頂いています。

そんなMirageですが。まず言っておかなければならないのは、MirageはORマッパーではありません。ORMというのは、オブジェクトの世界とリレーショナル(RDB)の世界を繋ぐ役割を持っています。つまり、テーブル間の関連を、その結果をオブジェクトのコンポジション(has)の状態と対応付けるのがORMです。その結果、ORMは「どのテーブルをどのようにJOINし、その結果をどのようにオブジェクトとして表現するのか」といった領域をサポート *1します。

それに対し、Mirageは関連を扱いません。SQLは自分で書きます。つまり、どのテーブルをどのようにJOINするのかは自分で決めます。RDBMSは、SELECT文に対して「行(row)の集合(set)」を返します。Mirageは、このをオブジェクトに対応付ける役割を担います。言ってみれば、(Object-)Row-Mapper *2と言えるでしょう。

というわけで、能書きはこのくらいにして、サンプルコードいきましょうか。

サンプルコード

テーブル定義

まず、テーブルを作っておきましょう。ここではMySQLを利用しました。適当にこのくらい、作っておきましょう。

CREATE TABLE users (
	username VARCHAR(32) PRIMARY KEY,
	password CHAR(60) NOT NULL
);

CREATE TABLE events (
	event_id BIGINT AUTO_INCREMENT PRIMARY KEY,
	event_name VARCHAR(128) NOT NULL,
	event_owner VARCHAR(32),
	FOREIGN KEY (event_owner) REFERENCES users (username)
);

CREATE TABLE participations (
	username VARCHAR(32),
	event_id BIGINT,
	PRIMARY KEY (username, event_id),
	FOREIGN KEY (username) REFERENCES users (username),
	FOREIGN KEY (event_id) REFERENCES events (event_id)
);

CREATE TABLE pictures (
	picture_id BIGINT AUTO_INCREMENT PRIMARY KEY,
	location VARCHAR(255) NOT NULL,
	event_id BIGINT NOT NULL,
	FOREIGN KEY (event_id) REFERENCES events (event_id)
);

INSERT INTO users
	(username, password)
VALUES
	('miyamoto', '$2a$10$cPnF0sq.bCPHeGuzVagOgOmbe2spT1Uh1k9LyuS0jzb5F3Lm.9kEy'),
	('yokota',   '$2a$10$nkvNPCb3Y1z/GSearD7s7OBdS9BoLBss3D4enbFQIgNJDvr0Xincm');

ちなみに、DBに生パスワードを保存するという設計は、やってはいけません。上記はエンコード(BCrypt)によって加工したパスワードを記述しています。

接続情報の定義

続いて、Javaクラスパスのルートにjdbc.propertiesというファイルを配置します。内容は下記のような感じ。DBへの接続情報ですね。各自の環境に合わせて、適宜書き換えてください。

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/partyalbum
jdbc.user=root
jdbc.password=

usersテーブル用のSQLを定義

次に、SQLファイルを用意します。まずは普通に、JOIN無しで、普通にユーザを検索するようなSQLを書きます。これもjdbc.propertiesと同じ場所にselect-user.sqlとして保存してください。

SELECT *
FROM users

/*BEGIN*/
WHERE

  /*IF username != null */
  username = /*username*/'miyamoto'
  /*END*/

  /*IF password != null */
  AND password = /*password*/'$2a$10$cPnF0sq.bCPHeGuzVagOgOmbe2spT1Uh1k9LyuS0jzb5F3Lm.9kEy'
  /*END*/

/*END*/

ORDER BY username

さて、何やら妙なコメントがいっぱいついたSQLです。これがMirageの特徴でもある「2WaySQL」と呼ばれるものです。このSQLのコメントを全て取り除くと、普通のSQL文として実行可能です。少々複雑なSQLを書いた時、そのまま実行してみることができるというのは便利そうですね。そしてこれらのコメントは、Mirageが実行する時に解釈されるディレクティブとしても機能します。というわけで「そのまま実行してみる為の役割」と「Mirageの為のテンプレートの役割」の2つを1つのSQLで担おう、というのが「2WaySQL」です。

文法の要点としては3つ。

  1. /*username*/'miyamoto'のように、値の直前にあるコメントは、直後の値もろとも、実行時に与えられるパラメータの値に置き換わります。正確に言えば、直後の値もろとも?というprepared statementのプレースホルダに変換され、パラメータとしてusernameの値が設定されます。
  2. /*IF username != null *//*END*/のように、IFで始まるコメントは条件分岐で、後続の式が真となった時のみ、この範囲が有効になります。
  3. /*BEGIN*//*END*/は少々複雑ですが、これが必要となる状況を考えてみると分かりやすい。上記の2つのIFが両方とも偽となった場合、WHEREが邪魔になってSQLが文法エラーを起こしてしまいます。そうならないよう、配下のIFが全てFALSEとなった時、それに伴って消える範囲を示しているのがBEGIN〜ENDです。この他にも、1つ目のIFが偽で、2つ目のIFが真になったらANDが邪魔…という問題がありますが、その辺はBEGIN〜END、の範囲に入っているものを自動的に認識して、Mirageが上手くやってくれます *3

mirageの依存ライブラリ定義

さて、あとはMirageの依存ライブラリを定義 *4して、いよいよプログラミングです。

  <dependencies>
    <dependency>
      <groupId>jp.sf.amateras.mirage</groupId>
      <artifactId>mirage</artifactId>
      <version>1.2.0</version>
    </dependency>
  </dependencies>
  <repositories>
    <repository>
      <id>amateras</id>
      <name>amateras</name>
      <url>http://amateras.sourceforge.jp/mvn</url>
    </repository>
  </repositories>

エンティティクラス定義

まず、select-user.sqlの実行結果の「行」に対応するクラスを下記のように定義します。カラム名とフィールド名が一致しているので、本当は@Columnアノテーションは不要なんですが、個人的にあったほうが好きなので書いてます。テーブル名(users)とクラス名(User)は一致していないので、こちらは必要ですが、一致しているのであれば不要です。が、あった方が好きなので私は結構常に書くと思います。

@SuppressWarnings("serial")
@Table(name = "users")
public class User implements Serializable {

  @PrimaryKey(generationType = GenerationType.APPLICATION)
  @Column(name = "username")
  private String username;

  @Column(name = "password")
  private String password;

  // getter / setter / toString省略
}

トランザクションの制御

さて、DBにクエリを投げる前に、そのトランザクションの制御が必要ですね。ということで、最も原始的な書き方ですが、基本はこんな感じのコードになります。宣言的に書く方法は、下の方で紹介しますね :)

Session session = SessionFactory.getSession();
SqlManager sqlManager = session.getSqlManager();

session.begin();
try {
  // ※ ここでSQLを投げたり結果を受け取ったりする
  session.commit();
} catch (Exception ex) {
  session.rollback();
} finally {
  session.release();
}

SELECTしてみる

あとは ※ の部分でクエリを投げるコードです。まず、用意したSQLファイルを表すSqlResourceを作成しておきます。この状態で、usernameパラメータに値を指定して、単行検索をしてみましょう。今回はSqlManager#getSingleResultを呼び出します。パラメータとしてMapを作って渡しています。ここは、Java beansを定義して渡しても構いません。お好みの方でどうぞ。

Map<String, Object> params = new HashMap<>();
params.put("username", "miyamoto");
User miyamoto = sqlManager.getSingleResult(User.class, selectUserSql, params);
System.out.println(miyamoto);

続きまして、全件検索をしてみましょう。結果は複数になりますので、SqlManager#getResultListを呼び出します。第2引数は行に対応するエンティティクラス、第2引数はSqlResourceです。第3引数を省略すれば全てnullになります。上記のSQLで、usernameもpasswordもnullであれば、全件検索になりますね。

SqlResource selectUserSql = new ClasspathSqlResource("select-user.sql");
List<User> result = sqlManager.getResultList(User.class, selectUserSql);
for (User user : result) {
  System.out.println(user);
}

しかしこれ、全件がン千万件あったらメモリ破綻するパターンですね。件数が予想できない場合は全部のUserインスタンスを作る、というのは恐いものです。そんな場合はコレ。

sqlManager.iterate(User.class, new IterationCallback<User, Void>() {

  @Override
  public Void iterate(User entity) {
    System.out.println(entity);
    return null;
  }
}, selectUserSql);

UPDATEしてみる

updateもこんな感じ。

// パスワードの変更
miyamoto.setPassword("$2a$10$TmZ76JKCeNdYaz2MjGCwPOjQgZ6I/ginWN3.rt2ZWa59O0ZTHM67y");
sqlManager.updateEntity(miyamoto);

INSERTしてみる

続いて、Eventをinsertしてみましょうか。エンティティを定義して…

@SuppressWarnings("serial")
@Table(name = "events")
public class Event implements Serializable {
  
  @PrimaryKey(generationType = GenerationType.IDENTITY)
  @Column(name = "event_id")
  private long eventId;
  
  @Column(name = "event_name")
  private String eventName;
  
  @Column(name = "event_owner")
  private String eventOwner;

  // コンストラクタ / getter / setter / toString等省略
}

こんな感じ。

Event event = new Event("パーティー", miyamoto.getUsername());
sqlManager.insertEntity(event);

IDは自動採番になってますが、きちんと設定されてますよ。insertEntityの前後で、event.getEventId()してみると、insert前は0ですが、insert後は1等となるはずです。これは @PrimaryKey(generationType = GenerationType.IDENTITY) アノテーションの仕業です。

SQLレス検索と、DELETE

上記の通り、SELECTは基本的にSQLファイルを使うのですが、SQL無しで検索も可能です。

User yokota = sqlManager.findEntity(User.class, "yokota");

そして、insert / update / select ときたら delete ですね。もちろん可能です。

sqlManager.deleteEntity(yokota);

これらの SqlManager#xxxEntity()系のメソッドは、SQLファイルは一切不要で、アノテーションベースの情報だけで動きます。

Springとの連携

基本機能は以上の通りですが、生で使うと少々使いづらいですよね。いまどきトランザクションを手続き的に書いたりしないですね。ということで、さらっとSpring連携をご紹介。

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
  <property name="driverClassName" value="com.mysql.jdbc.Driver" />
  <property name="url" value="jdbc:mysql://localhost:3306/partyalbum" />
  <property name="username" value="root" />
  <property name="password" value="" />
</bean>

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource"/>
</bean>

<bean id="connectionProvider" class="jp.sf.amateras.mirage.integration.spring.SpringConnectionProvider">
  <property name="transactionManager" ref="transactionManager" />
</bean>

<bean id="dialect" class="jp.sf.amateras.mirage.dialect.MySQLDialect" />

<bean id="sqlManager" class="jp.sf.amateras.mirage.SqlManagerImpl">
  <property name="connectionProvider" ref="connectionProvider" />
  <property name="dialect" ref="dialect" />
</bean>

これだけでOK。これにより、SessionFactorySessionは見えない所に隠れます。ちなみにこのパターンではjdbc.propertiesは不要です。また、SqlManagerがbeanとして登録されますので、下記のようにDIして使えますね。

@Component
public class UserService {

  @Autowired
  SqlManager sqlManager;
  
  @Transactional
  public void doService() {
    // メソッドの in/out がトランザクション境界
  }
}

SqlManagerの呼び出しはトランザクションの中にある必要があるので、@Transactionalアノテーションが必須になります。これにより、そのメソッドの実行開始と終了のタイミングで、トランザクションの開始とコミットが行われるようになる次第です。これが、宣言的トランザクション記述。

まとめ

さて。Springを利用することにより、少々使いやすくなったと思います。JPA等とはまた違った、ライトウェイトなデータアクセスライブラリの選択肢として、あなたの道具箱にも是非加えておいてはいかがでしょうか。ちなみに、私は使ったことがないのですが、作者の方はScala大好きっぽいので、mirage-scalaというモジュールも作っているようです。Scala好きな方は、そちらも目を通してみることをお薦めいたします。

では次回は! MirageとSpringの連携をもっと密にして、使いやすいMirageの世界をSpring連載の第四〜五回辺りでご紹介しようと思っています。Mirageの真価が発揮されるのは、実はSpring環境下なのです。お楽しみに!

脚注

  1. 語弊を恐れず言うのであれば「自動的に決定」と言い換えることもできます。
  2. あれ、略すとORMだ…。こりゃいかん…。
  3. トークンがAND, OR, ,から始まった場合が調整の対象。
  4. 以下はmavenの場合。gradle使いは上手くやってください。