JPAは便利そうだけど難しそう。そんなイメージがあるかどうかは分かりませんが、ClojureからClojure-wayで割と簡単に使えるようにしたので、使い方を書き起こしておきます。
事前準備
-
Entityクラスを用意します.
@Entity public class User implements Serializable { @Id @GeneratedValue private Long id; private String familyName; private String lastName; // 誌面の都合上、getter/setterは省いてます }
-
persistence.xml
を書きます.(現在の仕様はPersistenceUnitの名前は"default"固定です)<?xml version="1.0" encoding="UTF-8"?> <persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"> <persistence-unit name="default" transaction-type="RESOURCE_LOCAL"> <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> <shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode> <properties> <property name="eclipselink.target-database" value="org.eclipse.persistence.platform.database.H2Platform"/> <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/> <property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:db;create=true"/> <property name="javax.persistence.jdbc.user" value="APP"/> <property name="javax.persistence.jdbc.password" value="APP"/> <property name="eclipselink.logging.level" value="FINE"/> <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/> </properties> </persistence-unit>
```clj
[clj-jpa "0.1.0"]
```
使うJPAの依存も追加します。上記persistence.xmlの記述例の場合はH2データベースとEclipseLinkを使っているので↓のようにします。
```clj
[org.eclipse.persistence/org.eclipse.persistence.jpa "2.5.2"]
[org.jboss.weld.se/weld-se "2.2.14.Final"]
[com.h2database/h2 "1.4.188"]
```
EntityManager
(require '[clj-jpa.core :refer :all])
(with-entity-manager
;;; データベースアクセスコード
)
この中でEntityManagerを使った操作が可能になります。
トランザクション
(require '[clj-jpa.core :refer :all])
(with-transaction
;;; トランザクションコード
)
with-transaction
で囲うと、そこがトランザクション境界になります。JPAのトランザクションと同じく、同一EntityManager下においてネストはできません。
検索
JPAのCriteria APIとJPQL、Native SQLが使えます。
Criteria API
whereオプションで条件式を渡すことができます。
(require '[clj-jpa.entity-manager :as em])
(with-entity-manager
(em/search User :where (= :family-name "Kawashima")))
検索結果はkebab-caseのマップとして帰ってきます。
[{:family-name "Kawashima" :last-name "Yoshitaka"}]
Entityの関連がある場合は、Delayになっています。
[{:family-name "Kawashima" :groups #object[clojure.lang.Delay 0x6dc30289 {:status :pending, :val nil}]}]
Lazy fetchの場合は読みだそうとしたタイミングでクエリが投げられ取得できます。
=> (count (get-in users [0 :groups 0 :name]))
group1
korma
と同じ条件式をサポートしています。
(with-entity-manager
(em/search User :where (and (like :last-name "Kawashima")
(>= :age 40))
JOINは未サポートです。
JPQL
:jpql
でJPQL文を、:params
でバインド変数を渡すとJPQLのクエリが実行されます。
(with-entity-manager
(em/search User
:jpql "SELECT u FROM User u WHERE u.familyName = :name"
:params {:name "kawasima"}))
Native SQL
:sql
でネイティブのSQL文を、:params
でバインド変数を渡すとNative SQLのクエリが実行されます。
(with-entity-manager
(em/search User
:sql "SELECT * FROM user WHERE familyname = :name"
:params {:name "kawasima"}))
更新
(with-entity-manager
(with-transaction
(em/merge User {:family-name "Kawashima"
:last-name "Yoshitaka"}))
clj-jpaのAPIを使って検索したMapは、エンティティクラスを省略してmerge
を呼ぶことが可能です。
(with-entity-manager
(with-transaction
(let [user (em/find User 1)]
(em/merge (update-in user [:age] inc)))))
Ringで使う
ring用のhandlerが同梱されているので、compojureなどのring上のフレームワークで簡単にJPAを使うことができるようになります。
(require '[clj-jpa.middleware :refer [wrap-entity-manager wrap-transaction]])
(-> handler
wrap-transaction
wrap-entity-manager
(wrap-defaults site-defaults))
wrap-transactionの方を、wrap-entity-managerの内側に書く必要があります。
まとめ
Clojureで広く使われているDBアクセスライブラリは、korma
あたりかと思います。Clojureの志向的にテーブルの1行はマップで表されるのですが、とはいえデータアクセスレイヤには型があると安心感があります。
clj-jpaは、マップで扱える手軽さはそのままに、DBとのマッピングはEntityクラスでかっちりおこなうというところを目指しています。
JPAがバックグラウンドにあることで、Lazyローディングやスキーマの自動生成などもそのまま利用できることも魅力になるかと思います。