はじめに
Exposedを使う場合にDSLとDAOのどちらを使えば良いのか?
について整理したことを実行例を交えてメモしておきます。
Exposedとは
Kotlinで書かれたORMフレームワークです。
Hibernateなどに相当しますがより簡潔に利用できる感じがします。
DSL形式とDAO形式の2通りのアクセス方法をサポートしているのが特徴なのですが、
そのせいで『どっちを使うのが良いの?』と迷いがちです。(迷った)
環境
- Kotlin:1.3.61
- Exposed:0.25.1
- H2:1.4.199
- Intelli J:2019.3(Community Edition)
そもそも勘違いしていたこと
「DSLとDAOの二種類の方法がある」とあるため、これらは動作モードのようなもので排他だと思っていたのですが、実は共存/併用が可能でした。
クラス構成と合わせて考えるとこんな感じです。
その上でザックリと結論
以下の特徴と自分のユースケースを考えて好きな方を選べば良い
DSLの特徴
- SQLに似た構文のDSLを使うためSQLに慣れた人には簡単
- DSLはタイプセーフなのでコンパイラによって型チェックされ安全
- (DAOに比較して)データベースに近い目線で操作する
- 複雑なクエリ操作が可能
DAO特徴
- シンプルなCRUD操作によりデータを簡単に扱える
- ボイラプレートやコードの重複が少ない
- (DSLに比較して)アプリケーションドメインに近い目線で操作する
- 複雑なクエリ操作は出来ない場合がある
実行例
サンプルデータ
今回はサンプルとして以下のようなデータを使います。
ID | タイトル | 著者 | 価格 |
---|---|---|---|
1 | アジャイル開発とスクラム | 平鍋 | 2,000 |
2 | Java3Dプログラミング・バイブル | 平鍋 | 1,580 |
3 | 受託開発の極意 | 岡島 | 1,480 |
テーブルの定義
DSL/DAOに共通の準備としてテーブルを定義します。
object Books: IntIdTable() {
val title = varchar("title", 50)
val author = varchar("author", 50)
val price = integer("price")
}
エンティティの定義
更にDAOで扱うためにエンティティも定義します。
class Book(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<Book>(Books)
var title by Books.title
var author by Books.author
var price by Books.price
}
各種操作
Create, Drop
DSL/DAOに関係なく共通の書き方になります。
transaction {
SchemaUtils.drop (Books)
SchemaUtils.create (Books)
}
内部ではこのようなSQLが実行されます。
SQL: DROP TABLE IF EXISTS BOOKS
SQL: CREATE TABLE IF NOT EXISTS BOOKS (ID INT AUTO_INCREMENT PRIMARY KEY, TITLE VARCHAR(50) NOT NULL, AUTHOR VARCHAR(50) NOT NULL, PRICE INT NOT NULL)
Insert
DSL
テーブルに対してinsert()
を使用します。
transaction {
Books.insert {
it[title] = "アジャイル開発とスクラム"
it[author] = "平鍋"
it[price] = 2000
}
Books.insert {
it[title] = "Java3Dプログラミング・バイブル"
it[author] = "平鍋"
it[price] = 1580
}
Books.insert {
it[title] = "受託開発の極意"
it[author] = "岡島"
it[price] = 1480
}
}
DAO
new()
を使用してエンティティを作成します。
transaction {
Book.new {
title = "アジャイル開発とスクラム"
author = "平鍋"
price = 2000
}
Book.new {
title = "Java3Dプログラミング・バイブル"
author = "平鍋"
price = 1580
}
Book.new {
title = "受託開発の極意"
author = "岡島"
price = 1480
}
}
どちらも同一のSQLが実行されます。
SQL: INSERT INTO BOOKS (AUTHOR, PRICE, TITLE) VALUES ('平鍋', 2000, 'アジャイル開発とスクラム')
SQL: INSERT INTO BOOKS (AUTHOR, PRICE, TITLE) VALUES ('平鍋', 1580, 'Java3Dプログラミング・バイブル')
SQL: INSERT INTO BOOKS (AUTHOR, PRICE, TITLE) VALUES ('岡島', 1480, '受託開発の極意')
Select(全件)
DSL
テーブルに対してselectAll()
を使用します。
transaction {
val books: Query = Books.selectAll()
books.forEach { book -> println("Books = $book") }
}
SQL: SELECT BOOKS.ID, BOOKS.TITLE, BOOKS.AUTHOR, BOOKS.PRICE FROM BOOKS
Books = Books.id=1, Books.title=アジャイル開発とスクラム, Books.author=平鍋, Books.price=2000
Books = Books.id=2, Books.title=Java3Dプログラミング・バイブル, Books.author=平鍋, Books.price=1580
Books = Books.id=3, Books.title=受託開発の極意, Books.author=岡島, Books.price=1480
DAO
エンティティに対してall()
を使用します。
transaction {
val books: SizedIterable<Book> = Book.all()
books.forEach { book -> println("Books = $book") }
}
SQL: SELECT BOOKS.ID, BOOKS.TITLE, BOOKS.AUTHOR, BOOKS.PRICE FROM BOOKS
Books = Book@479460a6
Books = Book@7164ca4c
Books = Book@4f3bbf68
all
で返却されるのはエンティティであるBook
型のIterableになります。
Select(条件付き)
DSL
テーブルに対してselect { Op <Boolean> }
を使用します。
ここでOp
は検索条件になります。
transaction {
val book: ResultRow = Books.select{ Books.price eq 2000}.single()
println("1st book is $book")
println("1st book title is ${book[Books.title]}")
}
SQL: SELECT BOOKS.ID, BOOKS.TITLE, BOOKS.AUTHOR, BOOKS.PRICE FROM BOOKS WHERE BOOKS.PRICE = 2000
1st book is Books.id=1, Books.title=アジャイル開発とスクラム, Books.author=平鍋, Books.price=2000
1st book title is アジャイル開発とスクラム
select
で返却されるのはQuery
型で、single()
を使用してResultRow
型に変換できます。
ResultRow
型に対しては、[テーブル.メンバ名]
でアクセスできます。
DAO
エンティティに対してfind { Op <Boolean> }
を使用します。
ここで条件にテーブル定義であるBooks.price
を書かなければいけないのがちょっと残念な感じです。
transaction {
val book = Book.find{ Books.price eq 1480 }.single()
println("1st book is $book")
println("1st book title is ${book.title}")
}
SQL: SELECT BOOKS.ID, BOOKS.TITLE, BOOKS.AUTHOR, BOOKS.PRICE FROM BOOKS WHERE BOOKS.PRICE = 1480
1st book is Book@5c2375a9
1st book title is 受託開発の極意
find
で返却されるのはエンティティであるBook
型になります。
普通にクラスのメンバとしてアクセスできます。
Update
DSL
テーブルに対してupdate { Op <Boolean> }
を使用します。
transaction {
Books.update({Books.title eq "アジャイル開発とスクラム"}) {it[author] = "Hiranabe"}
}
SQL: UPDATE BOOKS SET AUTHOR='Hiranabe' WHERE BOOKS.TITLE = 'アジャイル開発とスクラム'
DAO
find { Op <Boolean> }
を使用して取得したエンティティのメンバを更新します。
transaction {
Book.find { Books.title eq "アジャイル開発とスクラム" }.single().apply {
author = "Hirabane"
}
}
SQL: SELECT BOOKS.ID, BOOKS.TITLE, BOOKS.AUTHOR, BOOKS.PRICE FROM BOOKS WHERE BOOKS.TITLE = 'アジャイル開発とスクラム'
SQL: UPDATE BOOKS SET AUTHOR='Hirabane' WHERE ID = 1
Delete(全件)
DSL
テーブルに対してdeleteAll()
を使用します。
transaction {
Books.deleteAll()
}
SQL: DELETE FROM BOOKS
DAO
all()
で全件取得した各エンティティに対してdelete()
を使用します。
transaction {
Book.all().forEach { it.delete() }
}
SQL: SELECT BOOKS.ID, BOOKS.TITLE, BOOKS.AUTHOR, BOOKS.PRICE FROM BOOKS
SQL: DELETE FROM BOOKS WHERE BOOKS.ID = 1
SQL: DELETE FROM BOOKS WHERE BOOKS.ID = 2
SQL: DELETE FROM BOOKS WHERE BOOKS.ID = 3