7
3

More than 3 years have passed since last update.

KotlinのExposedでDSLとDAOのどちらを使うべきか?

Last updated at Posted at 2020-06-11

はじめに

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の二種類の方法がある」とあるため、これらは動作モードのようなもので排他だと思っていたのですが、実は共存/併用が可能でした。

クラス構成と合わせて考えるとこんな感じです。

image.png

その上でザックリと結論

以下の特徴と自分のユースケースを考えて好きな方を選べば良い

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

参考情報

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3