Scala ActiveRecord

Post on 02-Jul-2015

4.193 views 1 download

Transcript of Scala ActiveRecord

Scala ActiveRecordThe elegant ORM library for Scala

Author主にScalaとRuby on Railsの業務をやってます

Play Framework 2.0 を Beta バージョンから業務で採用を試みる

DBライブラリにはSqueryl, Anorm, ScalaQuery を採用

Scala ActiveRecord はより使えるDBライブラリを求めた結果の産物

github.com/y-yoshinoya

概要Summary

Scala ActiveRecordhttps://github.com/aselab/scala-activerecord

Latest version: 0.2.1License: MIT

FeaturesSqueryl wrapperType-safe (most part)Rails ActiveRecord-like operability

CoC (Convention over Configuration)

DRY (Don't Repeat Yourself) principles.

Auto transaction control

“Type-safed ActiveRecord model for Scala”

Version 0.1

FeaturesValidationsAssociationsTesting supportImproving query performanceScala 2.10 support

Version 0.2

背景Background

Why created? (1)Scalaの大半のDBライブラリはSQLをWrapしたもの

関数型言語としての方向性としては正しい、かつ合理的な方法

しかし、オブジェクト (Model) にマッピングするためには全て自前で定義しないといけない

val selectCountries = SQL("Select * from Country") val countries = selectCountries().map(row => row[String]("code") -> row[String]("name")).toList

Not DRY!!

Why created? (2)関連マッピングをまともに扱いたい・なるべく簡単に使いたい

ClassごとにFinder methodを定義しなければならないなどDRYに書けない

本質的な処理についてだけ記述したいのにできない。操作を書きたいのであってSQLを書きたいわけではない

Not DRY!!!

Other librariesAnormSlick (ScalaQuery)Squeryl

(1) AnormORMではなく、Model層を提供しない設計思想のため、どうしてもClassごとに同じようなメソッドを定義せざるを得なくなる

Not DRY...

case class Person(id: Pk[Long], name: String) object Person { def create(person: Person): Unit = { DB.withConnection { implicit connection => SQL("insert into person(name) values ({name})").on( 'name -> person.name).executeUpdate() } } ...}

(2) Slick (ScalaQuery)Queryの使用感は良いが、テーブル定義がやや冗長。Modelとマッピングする場合、その対応をテーブルごとに明示的に記述する必要がある

Query interface is Good. But, not DRY defining tables.

case class Member(id: Int, name: String, email: Option[String])

object Members extends Table[Member]("MEMBERS") { def id = column[Int]("ID", O.PrimaryKey, O.AutoInc) def name = column[String]("NAME") def email = column[Option[String]]("EMAIL") def * = id.? ~ name ~ email <> (Member, Member.unapply _) }

val query = from(table)(t => where(t.id.~ > 20) select(t))from(query)(t => where(t.name like “%test%”) select(t))

(3) SquerylScalaのORMとしては最も良い出来

Queryに対してさらに条件を指定したQueryを作成するとSub-QueryなSQLが呼び出される

Very nice ORM library.Need to be aware of the SQL performance.

Select * From (Select * From table Where table.id > 20) q1Where q1.name like “test”

Improvements from SquerylQueryの合成結果が単なるSub Queryにならないように   Queryの条件をパフォーマンス劣化せず流用可能

val query = Table.where(_.id.~ > 20)query.where(_.name like “%test%”).toList

Select * From table Where table.id > 20 and table.name like “test”

Generates more simple SQL statement.

Improvements from SquerylIterable#iterator にアクセスした時点で inTransaction するよう変更

save, delete 時にデフォルトで inTransaction するように

もちろん明示的に transaction もできる

// auto inTransactionquery.toListmodel.savemodel.delete

Improvements from Squeryl関連設定ルールをCoCで結び付けられるように

関連参照時のQueryがSubQueryにならないように

Eager loadingを実装

Simpler association definition rule.

object Schema extends Schema { val foo = table[Foo] val bar = table[Bar] val fooToBar = oneToManyRelation(Foo, Bar).via( (f, b) => f.barId === b.id )}

class Foo(var barId: Long) extends SomeEntity {  lazy val bar: ManyToOne[Bar] = schema.fooToBar.right(this)}

class Bar(var bar: String) extends SomeEntity {  lazy val foos: OneToMany[Foo] = schema.fooToBar.left(this)}

Association definition (Squeryl)

object Tables extends ActiveRecordTables { val foo = table[Foo] val bar = table[Bar]}

class Foo(var barId: Long) extends ActiveRecord {  lazy val bar = belongsTo[Bar]}

class Bar(var bar: String) extends ActiveRecord {  lazy val foos = hasMany[Foo]}

Association definition (Scala ActiveRecord)

Minimal example

case class Person(var name: String, var age: Int) extends ActiveRecord

object Person extends ActiveRecordCompanion[Person]

Model implementation

Schema definitionobject Tables extends ActiveRecordTables { val people = table[Person]}

val person = Person("person1", 25)person.save true

Create

val person = Person("person1", 25).create Person(“person1”, 25)

Person.findBy(“name”, “john”) Some(Person(“john”))

Read

Person.where(_.name === “john”).headOption Some(Person(“john”))

Person.toList List(Person(“person1”), ...)

* Type-safe approach

Person.find(1) Some(Person(“person1”))

UpdatePerson.find(1).foreach { p => p.name = “aaa” p.age = 19 p.save}

Person.forceUpdate(_.id === 1)( _.name := “aa”, _.age := 19)

Callback hookValidations

DeletePerson.where(_.name === “john”) .foreach(_.delete)

Person.delete(1)

Person.find(1) match { case Some(person) => person.delete case _ =>}

Query interface

Single object finderval client = Client.find(10) Some(Client) or None

val john25 = Client.findBy(("name", "john"), ("age", 25)) Some(Client("john", 25)) or None

val john = Client.findBy("name", "john") Some(Client("john")) or None

Multiple object finderClients.where(c => c.name === "john" and c.age.~ > 25).toList

Clients.where(_.name === "john") .where(_.age.~ > 25) .toList

Select clients.name, clients.age, clients.idFrom clientsWhere clients.name = “john” and clients.age > 25

Using `Iterable` methodsval client = Client.head First Client or RecordNotFoundException

val (adults, children) = Client.partition(_.age >= 20) Parts of clients

val client = Client.lastOption Some(Last Client) or None

Ordering

Client.orderBy(_.name)

Client.orderBy(_.name asc)

Client.orderBy(_.name asc, _.age desc)

* Simple order (ORDER BY client.name)

* Set order (use for 'asc' or 'desc')

* Ordering by multiple fields

Client.limit(10)

Client.page(2, 5)

Limit and Offset

Existence of objectsClient.exists(_.name like "john%") true or false

Client.select(_.name).toList List[String]

Selecting specific fields

Client.select(c => (c.name, c.age)).toList List[(String, Int)]

Combining QueriesClients.where(_.name like "john%”) .orderBy(_.age desc) .where(_.age.~ < 25) .page(2, 5) .toListSelect clients.name, clients.age, clients.idFrom clientsWhere ((clients.name like “john%”) and (clients.age < 25))Order By clients.age Desclimit 5 offset 2

Cache controlQueryからIterableに暗黙変換される際に取得したListをキャッシュとして保持

val orders = Order.where(_.age.~ > 20)

// execute SQL query, and cached queryorders.toList

// non-execute SQL query. orders.toList

Validations

Annotation-based Validation

case class User( @Required name: String, @Length(max=20) profile: String, @Range(min=0, max=150) age: Int) extends ActiveRecord

object User extends ActiveRecordCompanion[User]

Validation Sample// it’s not save in the database // because the object is not validval user = User("", “Profile”, 25).create

user.isValid falseuser.hasErrors trueuser.errors.messges Seq("Name is required")user.hasError("name") true

More functional error handling...User("John", “profile”, 20).saveEither match { case Right(user) => println(user.name) case Left(errors) => println(errors.messages)} "John"

User("", “profile”, 15).saveEither match { case Right(user) => println(user.name) case Left(errors) => println(errors.messages)} "Name is required"

Callbacks

Available hooks•beforeValidation()•beforeCreate()•afterCreate()•beforeUpdate()•afterUpdate()•beforeSave()•afterSave()•beforeDelete()•afterDelete()

Callback examplecase class User(login: String) extends ActiveRecord { @Transient @Length(min=8, max=20) var password: String = _ var hashedPassword: String = _

override def beforeSave() { hashedPassword = SomeLibrary.encrypt(password) }}

val user = User(“john”)user.password = “raw_password”user.save Storing encrypted password

Associations

case class User(name: String) extends ActiveRecord { val groupId: Option[Long] = None lazy val group = belongsTo[Group]}

case class Group(name: String) extends ActiveRecord { lazy val users = hasMany[User]}

One-to-Many

val user1 = User("user1").createval user2 = User("user2").createval group1 = Group("group1").create

group1.users << user1

group1.users.toList List(User("user1"))user1.group.getOrElse(Group(“group2”)) Group("group1")

One-to-Many

Association is Queryablegroup1.users.where(_.name like “user%”) .orderBy(_.id desc) .limit(5) .toList

Select users.name, users.idFrom usersWhere ((users.group_id = 1) AND (users.name like “user%”))Order By users.id Desclimit 5 offset 0

case class User(name: String) extends ActiveRecord { lazy val groups = hasAndBelongsToMany[Group]}

case class Group(name: String) extends ActiveRecord { lazy val users = hasAndBelongsToMany[User]}

Many-to-Many (HABTM)

val user1 = User("user1").createval user2 = User("user2").createval group1 = Group("group1").createval group2 = Group("group2").create

user1.groups := List(group1, group2)

user1.groups.toList List(Group(“group1”), Group(“group2”))group1.users.toList List(User(“user1”))

Many-to-Many (HABTM)

case class Membership( userId: Long, projectId: Long, isAdmin: Boolean = false) extends ActiveRecord { lazy val user = belongsTo[User] lazy val group = belongsTo[Group]}

Many-to-Many (hasManyThrough)* Intermediate table's model

case class User(name: String) extends ActiveRecord { lazy val memberships = hasMany[Membership] lazy val groups = hasManyThrough[Group, Membership](memberships)}

case class Group(name: String) extends ActiveRecord { lazy val memberships = hasMany[Membership] lazy val users = hasManyThrough[User, Membership](memberships)}

Many-to-Many (hasManyThrough)

case class Group(name: String) extends ActiveRecord { lazy val adminUsers = hasMany[User](conditions = Map("isAdmin" -> true))}

Conditions option

group.adminUsers << user user.isAdmin == true

ForeignKey optioncase class Comment(name: String) extends ActiveRecord { val authorId: Long lazy val author = belongsTo[User](foreignKey = “authorId”)}

Client.joins[Order]( (client, order) => client.id === order.clientId).where( (client, order) => client.age.~ < 20 and order.price.~ > 1000 ).select( (client, order) => (client.name, client.age, order.price)).toList

Joining tables

Select clients.name, clients.age, orders.priceFrom clients inner join orders on (clients.id = orders.client_id)Where ((clients.age < 20) and (groups.price > 1000))

Order.includes(_.client).limit(10).map { order => order.client.name}.mkString(“\n”)

Eager loading associations

Select orders.price, orders.id From orders limit 10 offset 0;

Select clients.name, clients.age, clients.idFrom clients inner join orders on (clients.id = orders.client_id)Where (orders.id in (1,2,3,4,5,6,7,8,9,10))

Solution to N + 1 queries problem

Future

Future prospects

Compile time validation (using macro)Serialization support Web framework support(Offers view helpers for Play 2.x and Scalatra)STI, Polymorphic Association

Compile time validation(using macro)

型安全性が確保できていない部分について Scala macro を利用した型安全化

ActiveRecord#findBy(key: String, value: Any)

Association( conditions: Map[String, Any], foreignKey: String)

Type-safe binding configuration

Not type-safe

Serialization support パーサを個別に定義することなく、モデルを定義するだけで済むように

Form Model

JSON

XML

MessagePackValidationView

Viewhelper

Bind

Web framework supportCRUD controller

Form helper

scala-activerecord-play2

scala-activerecord-scalatra

Code generator Controller, Model, View

scala-activerecord-play2-sbt-plugin

scala-activerecord-scalatra-sbt-plugin etc..

sbt generate scaffold Person name:string:required age:int

Thank you