LoginSignup
54
40

More than 5 years have passed since last update.

【Kotlin】ExposedでCRUDを試す

Last updated at Posted at 2018-02-20

Exposedとは?

ExposedはKotlinで書かれたSQLライブラリです。
JetBrainsが主体となって開発を行っています。
2018/02/19現在の最新版は0.9.1であり、prototypeであることを留意してください。

サポートしているDB

以下のRDBMSに対応しています。
- PostgreSQL
- MySQL
- Oracle
- SQLite
- H2
- SQL Server

主要なRDBMSに対応しているため、普段使用しているDBに適用することができます。

データアクセス方式

Exposedでは2つのデータアクセス方式を提供しています。

DSL

DSL(Domain Specific Language)はSQLに似た記述を行うことができます。
そのためSQLに触れたことのある人であれば直感的に書くことができるのではないかと思います。
またKotlinの型チェックがはたらくため、type safetyとなります。

DAO

DAO(Data Access Object)ではHibernateのようなORMと近い働きをします。
また軽量であることを謳っています。

CRUDを試す

ここからは基本的な使用方法を紹介していきます。

環境

  • Kotlin: 1.2.21
  • Exposed: 0.9.1
  • MySQL: 5.7.21

DBに関しましてはお好きなものに置き換えてお試しください。

依存関係追加

Gradle, Maven共にrepositoryとdependencyを追加するだけです。

Gradle
repositories {
    maven {
        url "https://dl.bintray.com/kotlin/exposed”
    }
}

dependencies {
    compile 'org.jetbrains.exposed:exposed:0.9.1
}
Maven
<repositories>
  <repository>
    <id>exposed</id>
    <name>exposed</name>
    <url>https://dl.bintray.com/kotlin/exposed</url>
  </repository>
</repositories>

<dependencies>
  <dependency>
    <groupId>org.jetbrains.exposed</groupId>
    <artifactId>exposed</artifactId>
    <version>0.9.1</version>
  </dependency>
</dependencies>

またお使いのDBのドライバも追加しておきましょう。
私の場合は以下のようになりました。

build.gradle
dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.21"
    compile "org.jetbrains.exposed:exposed:0.9.1"
    compile "mysql:mysql-connector-java:5.1.45"
}

Exposedを使うために必要なこと

Exposedを使うために3つ必要なことがあります。

  • 依存関係の追加
    前述の通りです
  • コネクション
    当然ですがDBとのコネクションが必要です。以下の記述を用います。
Database.connect("url", "driver", "user", "password")

// たとえばMySQLのexposed_sampleへ接続するには以下のように記述します。
Database.connect("jdbc:mysql://localhost/exposed_sample", "com.mysql.jdbc.Driver", "root", "password")
  • トランザクション
    Exposedでの操作はtransactionブロック内に記述する必要があります。
transaction {
    // ここに処理を書く
}

また必須ではありませんが以下の記述をしておくことでSQLの標準出力を表示することができます。

logger.addLogger(StdOutSqlLogger)

DSLでの操作

テーブル定義

カラーコード(主キー)と名前を持つcolorsテーブルと
カラーコード(主キー、外部キー)とRGB値を持つRgbsテーブルを定義します。

Colors.kt
object Colors: Table("colors") {
    val code = varchar("code", 6).primaryKey()
    val name = varchar("name", 255)
}
Rgbs.kt
object Rgbs: Table("rgbs") {
    val code = (varchar("code", 6) references Colors.code).primaryKey()
    val r = integer("r")
    val g = integer("g")
    val b = integer("b")
}

Exposedでテーブル定義をするにはTableクラスを継承したシングルトンオブジェクトを作成します。
デフォルトではオブジェクト名がそのままテーブル名として作成されるため、
上記ではTable("colors")とすることでColorsテーブルではなくcolorsテーブルが作成されます。

各プロパティがカラムに相当し、制約はメソッドチェインで記述していきます。

Create, Drop

transaction {
    create(Colors, Rgbs)
    drop(Colors, Rgbs)
}

このように1行で記述できます。
この際以下のSQLが実行されます。

SQL: CREATE TABLE IF NOT EXISTS colors (code VARCHAR(6) PRIMARY KEY, name VARCHAR(255) NOT NULL)
SQL: CREATE TABLE IF NOT EXISTS rgbs (code VARCHAR(6) PRIMARY KEY, r INT NOT NULL, g INT NOT NULL, b INT NOT NULL,  FOREIGN KEY (code) REFERENCES Colors(code) ON DELETE RESTRICT)
SQL: DROP TABLE  IF EXISTS Colors
SQL: DROP TABLE  IF EXISTS Rgbs

SQLからわかるように、デフォルトではIF NOT EXISTSのオプションがつきます。

Insert

transaction {
    Colors.insert {
        it[code] = "40E0D0"
        it[name] = "turquoise"
    }
    Rgbs.insert {
        it[code] = "40E0D0"
        it[r] = 64
        it[g] = 224
        it[b] = 208
    }
}
SQL: INSERT INTO colors (code, name) VALUES ('40E0D0', 'turquoise')
SQL: INSERT INTO rgbs (code, r, g, b) VALUES ('40E0D0', 64, 224, 208)

MySQLではINSERT IGNORE用の関数を利用出来ます。

transaction {
    Colors.insertIgnore {
        it[code] = "40E0D0"
        it[name] = "turquoise"
    }
}
SQL: INSERT IGNORE INTO colors (code, name) VALUES ('40E0D0', 'turquoise')

Select

transaction {
    val allColors = Colors.selectAll()
    val turquoise = Colors.select { Colors.name eq "turquoise" }
    val blackCode = Rgbs.select { (Rgbs.r eq 0) and (Rgbs.g eq 0) and (Rgbs.b eq 0) }
}
SQL: SELECT colors.code, colors.name FROM colors
SQL: SELECT colors.code, colors.name FROM colors WHERE colors.name = 'turquoise'
SQL: SELECT rgbs.code, rgbs.r, rgbs.g, rgbs.b FROM rgbs WHERE rgbs.r = 0 and rgbs.g = 0 and rgbs.b = 0

select()ではラムダの中に条件を指定してデータを取得します。
またselect()やselectAllはIterableを継承したQuery型を返すため、取得した結果をコレクション操作することが可能です。

Update

transaction {
    Colors.update({ Colors.name eq "turquoise" }) { it[name] = "ターコイズ" }
}
SQL: UPDATE colors SET name='ターコイズ' WHERE colors.name = 'turquoise'

Delete

transaction {
    Rgbs.deleteAll()
    Colors.deleteWhere { Colors.name eq "ターコイズ" }
    Colors.deleteIgnoreWhere { Colors.name eq "black" }
}
SQL: DELETE FROM rgbs
SQL: DELETE FROM colors WHERE colors.name = 'ターコイズ'
SQL: DELETE IGNORE FROM colors WHERE colors.name = 'black'

Join

transaction {
    (Rgbs innerJoin Colors)
        .slice(Colors.name, Rgbs.r, Rgbs.g, Rgbs.b)
        .select { Colors.name eq "ターコイズ" }
        .forEach{ println(it) }
}
SQL: SELECT colors.name, rgbs.r, rgbs.g, rgbs.b FROM rgbs INNER JOIN colors ON colors.code = rgbs.code WHERE colors.name = 'ターコイズ'

> colors.name=ターコイズ, rgbs.g=224, rgbs.r=64, rgbs.b=208

JoinはinnnerJoin, leftJoin, crossJoinのキーワードを用いることができます。
またslice()関数でJoin後に残すカラムを指定することができます。

DAOでの操作

テーブル定義

今度はUsersテーブルとCountriesテーブルを定義します。
両テーブルともInteger型のidを主キーに持ちます。

Users.kt
object Users : IntIdTable() {
    val name = varchar("name", 255)
    val country = reference("country", Countries)
    val age = integer("age")
}
Countries.kt
object Countries: IntIdTable() {
    val name = varchar("name", 255)
}

テーブル定義にはDSLと同様に、Tableクラスを継承したobjectを作成します。
ただし、今回はInteger型のidを主キーに持つテーブルですので、IntIdTableクラスを継承しています。

エンティティインスタンス

DAOではエンティティインスタンスを作成します。
Usersテーブルに対してUserインスタンス、Countriesテーブルに対してCountryインスタンスをそれぞれ以下のように作成します。

User.kt
class User(id: EntityID<Int>) : IntEntity(id) {
    companion object : IntEntityClass<User>(Users)

    var name by Users.name
    var country by Country referencedOn Users.country
    var age by Users.age
}
Country.kt
class Country(id: EntityID<Int>) : IntEntity(id) {
    companion object : IntEntityClass<Country>(Countries)

    var name by Countries.name
    val users by User referrersOn Users.country
}

エンティティクラス作成時のポイントとして、先程作成したテーブル定義のオブジェクトに紐づくEntitiyClass型のcompanion objectを内包する必要があります。

Join

先に定義したUsersテーブルではcountryカラムに関して外部キー制約を以下のように設けていました。

val country = reference("country", Countries)

そのためUserインスタンスではcountryプロパティで以下のような記述を行いました。

var country by Country referencedOn Users.country

またCountriesテーブルではreferrersOnというキーワードと共にusersプロパティを定義しています。

val users by User referrersOn Users.country

これにより以下の記述で該当のCountryエンティティに紐づくUserデータを取得することができます。

// japanはCountryエンティティのインスタンス
japan.users

なお、referrersOnを用いる場合にはvalで定義する必要があります。

Create, Drop

こちらはDSLと同様です。

transaction {
    create (Countries, Users)
    drop (Countries, Users)
}

実行されるSQLは以下の通りです。

SQL: CREATE TABLE IF NOT EXISTS Users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, country INT NOT NULL, age INT NOT NULL,  FOREIGN KEY (country) REFERENCES Countries(id) ON DELETE RESTRICT)
SQL: CREATE TABLE IF NOT EXISTS Countries (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL)
SQL: DROP TABLE  IF EXISTS Users
SQL: DROP TABLE  IF EXISTS Countries

Insert

DAOではエンティティ作成にnewという拡張関数を使用します。

        val japan = Country.new {
            name = "Japan"
        }

        User.new {
            name = "gumimin"
            country = japan
            age = 25
        }
SQL: INSERT INTO Countries (name) VALUES ('Japan')
SQL: INSERT INTO Users (age, country, name) VALUES (25, 1, 'gumimin')

外部参照しているcountryプロパティは該当するidに変換してSQLが発行されるようです。

Select

transaction {
    println("Countries: ${Country.all().joinToString {it.name}}")
    println("Adults: ${User.find { Users.age greaterEq 18 }.joinToString {it.name}}")
}

all(), find(), findById()といった関数でデータを取得することができます。
こちらもDSL同様、返却されるデータの型はItelableを継承しています。

Update, Delete

transaction {
    val gumimin = User.find {Users.name eq "gumimin"}.single()
    gumimin.name = "choco"
    gumimin.delete()
}
SQL: SELECT Users.id, Users.name, Users.country, Users.age FROM Users WHERE Users.name = 'gumimin'
SQL: DELETE FROM Users WHERE Users.id = 1
SQL: UPDATE Users SET name='choco' WHERE id = 1

エンティティのプロパティを変更することで更新することができます。
こちらもSQLではWHERE句でidを用いているようです。

さいごに

本投稿ではExposedを用いてCRUDを試してみました。
感想としましては、DSLで直感的に記述できるところがいいなと感じました。
DAOにおいては、テーブルにidという名の主キーが存在しない場合にマッピングが煩雑になるため、そこら辺がもう少し簡素にできると使いやすいなと感じました。
あくまでも現状ではprototypeなので今後に期待していきたいと思います。

投稿内容に間違いなどございましたらご指摘頂けると幸いです。

54
40
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
54
40