Scala ActiveRecord

57
Scala ActiveRecord The elegant ORM library for Scala

Transcript of Scala ActiveRecord

Page 1: Scala ActiveRecord

Scala ActiveRecordThe elegant ORM library for Scala

Page 2: Scala ActiveRecord

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

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

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

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

github.com/y-yoshinoya

Page 3: Scala ActiveRecord

概要Summary

Page 4: Scala ActiveRecord

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

Latest version: 0.2.1License: MIT

Page 5: Scala ActiveRecord

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

Page 6: Scala ActiveRecord

FeaturesValidationsAssociationsTesting supportImproving query performanceScala 2.10 support

Version 0.2

Page 7: Scala ActiveRecord

背景Background

Page 8: Scala ActiveRecord

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!!

Page 9: Scala ActiveRecord

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

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

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

Not DRY!!!

Page 10: Scala ActiveRecord

Other librariesAnormSlick (ScalaQuery)Squeryl

Page 11: Scala ActiveRecord

(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() } } ...}

Page 12: Scala ActiveRecord

(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 _) }

Page 13: Scala ActiveRecord

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”

Page 14: Scala ActiveRecord

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.

Page 15: Scala ActiveRecord

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

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

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

// auto inTransactionquery.toListmodel.savemodel.delete

Page 16: Scala ActiveRecord

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

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

Eager loadingを実装

Simpler association definition rule.

Page 17: Scala ActiveRecord

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)

Page 18: Scala ActiveRecord

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)

Page 19: Scala ActiveRecord

Minimal example

Page 20: Scala ActiveRecord

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]}

Page 21: Scala ActiveRecord

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

Create

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

Page 22: Scala ActiveRecord

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”))

Page 23: Scala ActiveRecord

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

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

Callback hookValidations

Page 24: Scala ActiveRecord

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

Person.delete(1)

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

Page 25: Scala ActiveRecord

Query interface

Page 26: Scala ActiveRecord

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

Page 27: Scala ActiveRecord

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

Page 28: Scala ActiveRecord

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

Page 29: Scala ActiveRecord

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

Page 30: Scala ActiveRecord

Client.limit(10)

Client.page(2, 5)

Limit and Offset

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

Page 31: Scala ActiveRecord

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

Selecting specific fields

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

Page 32: Scala ActiveRecord

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

Page 33: Scala ActiveRecord

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

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

// execute SQL query, and cached queryorders.toList

// non-execute SQL query. orders.toList

Page 34: Scala ActiveRecord

Validations

Page 35: Scala ActiveRecord

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]

Page 36: Scala ActiveRecord

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

Page 37: Scala ActiveRecord

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"

Page 38: Scala ActiveRecord

Callbacks

Page 39: Scala ActiveRecord

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

Page 40: Scala ActiveRecord

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

Page 41: Scala ActiveRecord

Associations

Page 42: Scala ActiveRecord

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

Page 43: Scala ActiveRecord

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

Page 44: Scala ActiveRecord

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

Page 45: Scala ActiveRecord

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)

Page 46: Scala ActiveRecord

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)

Page 47: Scala ActiveRecord

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

Page 48: Scala ActiveRecord

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)

Page 49: Scala ActiveRecord

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”)}

Page 50: Scala ActiveRecord

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))

Page 51: Scala ActiveRecord

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

Page 52: Scala ActiveRecord

Future

Page 53: Scala ActiveRecord

Future prospects

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

Page 54: Scala ActiveRecord

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

Page 55: Scala ActiveRecord

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

Form Model

JSON

XML

MessagePackValidationView

Viewhelper

Bind

Page 56: Scala ActiveRecord

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

Page 57: Scala ActiveRecord

Thank you