Top Banner
Scala ActiveRecord The elegant ORM library for Scala
57

Scala ActiveRecord

Jul 02, 2015

Download

Documents

scalaconfjp
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
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