概要

自宅でさらっとプログラムを書く時なんかにはとても便利なORM(O/Rマッピング)。

Scalaで使う場合はScalaQueryとかSquerylあたりが有名だろうか。

今日は試しにSquerylの方を使ってデータベースの作成からデータの登録、更新、検索などを試してみた。

尚、本記事の内容はSquerylの公式サイトのチュートリアル的なものを参考にしながら書いている。ここに載っている内容はたいてい公式サイトにも載っている。

Introduction - Squeryl - A Scala ORM for SQL Databases
http://squeryl.org/introduction.html

@CretedDate 2012/01/11
@Versions Scala2.9.1, Squeryl0.9.4, MySQL5.1

導入

SBTかMavenを利用する場合は公式サイトに設定が載っている。

Getting Started - Squeryl - A Scala ORM for SQL Databases
http://squeryl.org/getting-started.html

Mavenの場合は下記のような感じ。

        <!-- scala-toolsをレポジトリに入れとく -->
        <repository>
            <id>scala-tools.org</id>
            <url>http://scala-tools.org/repo-releases</url>
        </repository>

        <!-- 中略 -->

        <dependency>
            <groupId>org.squeryl</groupId>
            <artifactId>squeryl_2.9.1</artifactId>
            <version>0.9.4</version>
        </dependency>

jarを直接落とす場合はどこが最新かよく分からなった。とりあえずMavenのレポジトリの中を覗けばいる。

http://scala-tools.org/repo-releases/org/squeryl/

Schemaの定義

まずはDBのカラムと対応するようなフィールドを持ったクラスを書く。

import org.squeryl.KeyedEntity

/** ユーザ名とテキストを保持するテーブルを定義 */
class Message(
  val user: String,
  val text: String,
  val id: Long = 0L ) extends KeyedEntity[Long]

上記はidusertextという3つのカラムを持った、Messageという名前のテーブルを定義している。

KeyedEntity[Long]をextendsしている。これはLongでautoinclementでプライマリーキーなidというカラムを定義してくれるクラス。

Stringのフィールドはvarchar(128)に変換される。

カラム名と変数名を別の名前にしたい場合は、下記のように@Columnアノテーションを使う。下記の例だと、textではなくmessage_textという名前でカラムが作成される。

import org.squeryl.annotations.Column

class Message2(
  val user: String,
  @Column("message_text")
  val text: String,
  val id: Long = 0L ) extends KeyedEntity[Long]

上記のクラスを使って、Schemaをextendsしたクラスの中でtableを定義する。

import org.squeryl.Schema

object DB extends Schema {
  val message = table[Message]
  val message2 = table[Message2]("message_two")
}

テーブル名はデフォルトだとクラス名と同名(Messageとか)になる。任意の名前を指定したい場合はmessage2のところで書いているように、tableの引数にテーブル名を渡す。

これでテーブルの定義はできた。次はDBに接続して実際にCreateするところ。

DBへの接続関連の記述

下記はMySQLを利用した例。

import org.squeryl.adapters.MySQLAdapter
import org.squeryl.{ SessionFactory, Session }

Class.forName( "com.mysql.jdbc.Driver" )
SessionFactory.concreteFactory = Some( () => Session.create(
  DriverManager.getConnection( "jdbc:mysql://localhost:3306/squeryl_test", "root", "" ),
  new MySQLAdapter ) )

こんな感じでconcreteFactoryに接続に関する記述をしておくと、あとは良い感じにConnectionを管理してくれるらしい。

テーブルの作成

テーブルのCREATEやDROPは上の方で書いたSchemaを継承したクラス(今回はDBという名前を付けたヤツ)から実行できる。

DBに対して行う処理は、PrimitiveTypeModeというクラスのtransactionの中に記述する。

import org.squeryl.PrimitiveTypeMode.transaction

transaction {
  // テーブルの作成
  DB.create
  // テーブルの削除
  DB.drop
}

上記のように実行すると、作成されるテーブルのカラムの順番は割と適当なことになる。

カラムの順番を変える方法は不明。こだわる場合はprintDDLでCREATE文を出力して順番を手描きで揃えて生SQLを実行すれば良いのだろうか。

// DDLの表示
DB.printDdl

// Messageテーブルはこんな感じのCREATE文になる
// create table Message (
//     text varchar(128) not null,
//     id bigint primary key not null auto_increment,
//     user varchar(128) not null
//   );

lift-mapper(LiftについてるORM)は配列にカラムの順番を書けばそれに揃えてくれた記憶がある。

createを実行すると、Schemaで定義されているテーブルがすべて(今回の例だとMessageとmessage_twoの2つ)作成される。

個別に作成/削除する場合は、たぶんこんな感じ?

DB.message.schema.drop
DB.message.schema.create

ここまでで書いたソースはこちら

もう少し詳細なテーブルの定義

上の例だとStringで定義したカラムの長さが128になっている。この辺を調節してみる。

詳細な定義はSchemaを継承したクラスの中で行う。

// PrimitiveTypeModeの中で定義されてるimplicit conversinsどもを使う
import org.squeryl.PrimitiveTypeMode._

object DB extends Schema {
  val message = table[Message]
  on( message )( m => declare(
    // uniqを指定してみる
    m.id is ( unique ),
    // indexを貼って、varchar(64)にしてみる
    m.user is ( indexed, dbType( "varchar(64)" ) ),
    // varchar(1024)にしてみる
    m.text is ( dbType( "varchar(1024)" ) )
  ) )
}

MySQLのTEXT型を使用してみようと思ったのだけど、見た限りではやり方が見つからなかった。

現状の設定だと、すべてのカラムがNOT NULLになっている。下記のようにフィールドの型をOptionにするとNULL規制が外れる。

class Message(
  val user: String,
  val text: Option[String] = None,
  val id: Long = 0L ) extends KeyedEntity[Long]

// 上記の指定だと、textはDEFAULT NULLになる

デフォルト値を入れたい場合は、defaultsToを使う。

object DB extends Schema {
  val message = table[Message]
  on( message )( m => declare(
    m.user defaultsTo( "no_name" ),
  ) )
}

// 上記の指定だと、userのデフォルト値はno_nameになる

ここまでで書いたソース

INSERT/DELETE/UPDATE

テーブルの作り方はなんとなく分かったので、データの登録とか更新とかをしてみる。

INSERTは簡単。insertメソッドにデータの入ったクラスを放り込むだけ。

transaction {
  DB.message.insert(new Message("mw", "hello, world"))
}

DELETEdeleteWhereを使えばいいらしい。比較は===演算子を使うようだ。

// id指定で削除
DB.message.deleteWhere( m => m.id === 1 )

// and条件も書ける
DB.message.deleteWhere( m => m.id === 2 and m.user === "mw" )

UPDATEもDELETEと同じような感じで書ける。

// idが3のレコードを更新
DB.message.update( new Message( "mw", "hello squeryl", 3L ) )

// where句を使ってSQLっぽく更新
update( DB.message )( m => where( m.id === 4L ) set ( m.text := "update squeryl" ) )

ここまでで書いたソース

SELECT

下記のような書き方でSELECTできるらしい。

// where句で条件指定してSELECT
val messages1 = from( DB.message )( m => where( m.user === "mw" ) select ( m ) )
  //=> Message(mw,update squeryl,4)
  //=> Message(mw,hello squeryl,3)

// 全行を取得する
val messages2 = DB.message.seq
  //=> Message(mw,update squeryl,4)
  //=> Message(mw,hello squeryl,3)

// キーを指定して1行取得
val message = DB.message.lookup( 3L )
  //=> Some(Message(mw,hello squeryl,3))

distinctとかorder byとかgroup byもしてみる。

// distinct
val messages3 = from( DB.message )( m => select( m.text ) ).distinct

// order by
val messages4 = from( DB.message )( m => select( m ) orderBy ( m.id asc ) )

// group by + count
val messages5 = from( DB.message )( m => groupBy( m.user ) compute ( m.user, count( m.id ) ) )

実際にどういったクエリが発行されているかは、statementで分かる。

val messages1 = from( DB.message )( m => where( m.user === "user_name" ) select ( m ) )

// 対応するSQLを表示
println(messages1.statement)

ここまでで書いたソース

JOIN

普通にwhereで結合したり、left outer joinを使ったり、いろいろできるらしい。

// whereで2つのテーブルをJOIN
val users1 = from( DB.userName, DB.userEmail )( ( name, mail ) =>
  where( name.id === mail.id ) select ( name.id, name.name, mail.email ) )

// fromではなくjoinメソッドを使いonで結合
val users2 = join( DB.userName, DB.userEmail )( ( name, mail ) =>
  select( name.id, name.name, mail.email ) on ( name.id === mail.id ) )

// joinの引数の中でleftOuterを指定して結合
val users3 = join( DB.userName, DB.userEmail.leftOuter )( ( name, mail ) =>
  select( name.id, name.name, mail.map( _.email ) ) on ( name.id === mail.map( _.id ) ) )

// fromとleftOuterJoinで結合
val user4 = from( DB.userName, DB.userEmail )( (name, mail) =>
  select (name.name, leftOuterJoin(mail, name.id === mail.id)) )

leftOuterを使った場合、外部結合されるテーブルはOption型で結果が返ってくる。ので、例えば上記の3つ目の例の変数mailはOption型になっている。

ここまでで書いたソース

Custom Function

SQLはDBMSごとにいろんな関数がいるけど、その辺を使う場合は自前で拡張しないといけないらしい。

試しにMySQLで以下のようなDateTime型を年に変換して比較するSQLを発行しようと思ったとする。

select * from User where year(birthday) = 2012

このyearという関数を実行したい場合は、以下のような記述を行う必要がある。

まず下準備として日付型を持っているテーブルを用意する。

// とりあえず日付を持っているテーブルを作る
case class User(
  val name: String,
  val birthday: Date,
  val id: Long = 0L ) extends KeyedEntity[Long]

object DB extends Schema {
  val user = table[User]
}

次にYearクラスを用意して、where句内で呼び出す。

// Dateを引数に取る、Int型のFunctionNode
class Year( e: DateExpression[Date] )
  extends FunctionNode[Int]( "year", e ) with NumericalExpression[Int]

// where句の中でnew YEAR(日付型)する
val users1 = from( DB.user )( name =>
  where( new Year( name.birthday ) === 2012 ) select ( name ) )

上記の記述はSQL的にはWHERE (year(birthday) = 2012)と同じ意味になる。

ちと面倒だけど、とりあえずこれで好きな関数が実行できる。

練習のためにもう1個くらい例を書いてみる。今度は文字数を数えるchar_length

// Stringを引数に取る、Int型のFunctionNode
class CharLength( e: StringExpression[String] )
  extends FunctionNode[Int]( "char_length", e ) with NumericalExpression[Int]

// char_length( name )=4的なSQLを作ってみる
val users2 = from( DB.user )( name =>
  where( new CharLength( name.name ) === 4 ) select ( name ) )

よし、なんか使い方覚えれた気がする。

ここまでで書いたソース

その他

「こんな機能ないのかな」と思った時はとりあえずGoogleグループのとこで検索してみる。たいていのことは既に誰かが質問している。

Squeryl | Google グループ
http://groups.google.com/group/squeryl

例1 : SQL直接実行できる? => できん

例2 : 生のConnectionの取り方教えて => とりあえずSession.currentConnection.connectionで

この辺のやりとりを見てると、かゆいところに手を伸ばそうとするとひと手間かかりそうな仕様のような気がする。その代わり、ひと手間かければある程度は思った通りのことができそうにも見える。