Scala ActiveRecord
-
Upload
scalaconfjp -
Category
Documents
-
view
4.193 -
download
1
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