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を追加するだけです。
repositories {
maven {
url "https://dl.bintray.com/kotlin/exposed”
}
}
dependencies {
compile 'org.jetbrains.exposed:exposed:0.9.1’
}
<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のドライバも追加しておきましょう。
私の場合は以下のようになりました。
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テーブルを定義します。
object Colors: Table("colors") {
val code = varchar("code", 6).primaryKey()
val name = varchar("name", 255)
}
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を主キーに持ちます。
object Users : IntIdTable() {
val name = varchar("name", 255)
val country = reference("country", Countries)
val age = integer("age")
}
object Countries: IntIdTable() {
val name = varchar("name", 255)
}
テーブル定義にはDSLと同様に、Tableクラスを継承したobjectを作成します。
ただし、今回はInteger型のidを主キーに持つテーブルですので、IntIdTableクラスを継承しています。
エンティティインスタンス
DAOではエンティティインスタンスを作成します。
Usersテーブルに対してUserインスタンス、Countriesテーブルに対してCountryインスタンスをそれぞれ以下のように作成します。
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
}
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なので今後に期待していきたいと思います。
投稿内容に間違いなどございましたらご指摘頂けると幸いです。