• Share
  • Email
  • Embed
  • Like
  • Save
  • Private Content
Scala ActiveRecord
 

Scala ActiveRecord

on

  • 3,605 views

 

Statistics

Views

Total Views
3,605
Views on SlideShare
2,689
Embed Views
916

Actions

Likes
5
Downloads
16
Comments
0

4 Embeds 916

http://yugolf.hatenablog.com 900
https://twitter.com 13
http://my.dudamobile.com 2
http://webcache.googleusercontent.com 1

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel

Scala ActiveRecord Scala ActiveRecord Presentation Transcript

  • Scala ActiveRecordThe elegant ORM library for Scala
  • Author github.com/y-yoshinoya主にScalaとRuby on Railsの業務をやってますPlay Framework 2.0 を Beta バージョンから業務で採用を試みる DBライブラリにはSqueryl, Anorm, ScalaQuery を 採用Scala ActiveRecord はより使えるDBライブラリを求めた結果の産物
  • 概要Summary
  • Scala ActiveRecordhttps://github.com/aselab/scala-activerecordLatest version: 0.2.1License: MIT
  • Features Version 0.1Squeryl wrapperType-safe (most part)Rails ActiveRecord-like operability CoC (Convention over Configuration) DRY (Dont Repeat Yourself) principles. Auto transaction control“Type-safed ActiveRecord model for Scala”
  • Features Version 0.2ValidationsAssociationsTesting supportImproving query performanceScala 2.10 support
  • 背景Background
  • Why created? (1)Scalaの大半のDBライブラリはSQLをWrapしたもの関数型言語としての方向性としては正しい、かつ合理的な方法 val selectCountries = SQL("Select * from Country") val countries = selectCountries().map(row => row[String]("code") -> row[String]("name") ).toListしかし、オブジェクト (Model) にマッピングするためには全て自前で定義しないといけない Not DRY!!
  • Why created? (2)関連マッピングをまともに扱いたい・なるべく簡単に使いたいClassごとにFinder methodを定義しなければならないなどDRYに書けない本質的な処理についてだけ記述したいのにできない。操作を書きたいのであってSQLを書きたいわけではない Not DRY!!!
  • Other libraries Anorm Slick (ScalaQuery) Squeryl
  • (1) AnormORMではなく、Model層を提供しない設計思想のため、どうしてもClassごとに同じようなメソッドを定義せざるを得なくなる 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() } } ... } Not DRY...
  • (2) Slick (ScalaQuery)Queryの使用感は良いが、テーブル定義がやや冗長。Modelとマッピングする場合、その対応をテーブルごとに明示的に記述する必要がある 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 _) } Query interface is Good. But, not DRY defining tables.
  • (3) Squeryl ScalaのORMとしては最も良い出来 Queryに対してさらに条件を指定したQueryを作成すると Sub-QueryなSQLが呼び出されるval query = from(table)(t => where(t.id.~ > 20) select(t))from(query)(t => where(t.name like “%test%”) select(t))Select * From (Select * From table Where table.id > 20) q1Where q1.name like “test” Very nice ORM library. Need to be aware of the SQL performance.
  • Improvements from SquerylQueryの合成結果が単なるSub Queryにならないように   Queryの条件をパフォーマンス劣化せず流用可能 val query = Table.where(_.id.~ > 20) query.where(_.name like “%test%”).toListSelect * From tableWhere table.id > 20 and table.name like “test” Generates more simple SQL statement.
  • Improvements from SquerylIterable#iterator にアクセスした時点で inTransaction するよう変更save, delete 時にデフォルトで inTransaction するように もちろん明示的に transaction もできる // auto inTransaction query.toList model.save model.delete
  • Improvements from Squeryl 関連設定ルールをCoCで結び付けられるように 関連参照時のQueryがSubQueryにならないように Eager loadingを実装 Simpler association definition rule.
  • Association definition (Squeryl)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(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] }
  • Minimal example
  • Model implementationcase class Person(var name: String, var age: Int) extends ActiveRecordobject Person extends ActiveRecordCompanion[Person] Schema definition object Tables extends ActiveRecordTables { val people = table[Person] }
  • Create val person = Person("person1", 25) person.save trueval person = Person("person1", 25).create Person(“person1”, 25)
  • ReadPerson.find(1) Some(Person(“person1”))Person.toList List(Person(“person1”), ...)Person.findBy(“name”, “john”) Some(Person(“john”))* Type-safe approachPerson.where(_.name === “john”).headOption Some(Person(“john”))
  • UpdatePerson.find(1).foreach { p => p.name = “aaa” p.age = 19 Callback hook p.save Validations}Person.forceUpdate(_.id === 1)( _.name := “aa”, _.age := 19)
  • DeletePerson.where(_.name === “john”) .foreach(_.delete)Person.find(1) match { case Some(person) => person.delete case _ =>}Person.delete(1)
  • Query interface
  • Single object finder val client = Client.find(10) Some(Client) or None val john = Client.findBy("name", "john") Some(Client("john")) or Noneval john25 = Client.findBy(("name", "john"), ("age", 25)) Some(Client("john", 25)) or None
  • Multiple object finderClients.where(c => c.name === "john" and c.age.~ > 25).toListClients.where(_.name === "john") .where(_.age.~ > 25) .toListSelect clients.name, clients.age, clients.idFrom clientsWhere clients.name = “john” and clients.age > 25
  • Using `Iterable` methodsval client = Client.head First Client or RecordNotFoundExceptionval client = Client.lastOption Some(Last Client) or Noneval (adults, children) = Client.partition(_.age >= 20) Parts of clients
  • Ordering* Simple order (ORDER BY client.name)Client.orderBy(_.name)* Set order (use for asc or desc)Client.orderBy(_.name asc)* Ordering by multiple fieldsClient.orderBy(_.name asc, _.age desc)
  • Limit and Offset Client.limit(10) Client.page(2, 5)Existence of objectsClient.exists(_.name like "john%") true or false
  • Selecting specific fieldsClient.select(_.name).toList List[String]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 query orders.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).createuser.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
  • One-to-Manycase 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-Manyval user1 = User("user1").createval user2 = User("user2").createval group1 = Group("group1").creategroup1.users << user1group1.users.toList List(User("user1"))user1.group.getOrElse(Group(“group2”)) Group("group1")
  • Association is Queryable group1.users.where(_.name like “user%”) .orderBy(_.id desc) .limit(5) .toListSelect users.name, users.idFrom usersWhere ((users.group_id = 1) AND (users.name like “user%”))Order By users.id Desclimit 5 offset 0
  • Many-to-Many (HABTM)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").createuser1.groups := List(group1, group2)user1.groups.toList List(Group(“group1”), Group(“group2”))group1.users.toList List(User(“user1”))
  • Many-to-Many (hasManyThrough)* Intermediate tables model 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)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)}
  • Conditions optioncase class Group(name: String) extends ActiveRecord { lazy val adminUsers = hasMany[User](conditions = Map("isAdmin" -> true))} group.adminUsers << user user.isAdmin == true ForeignKey optioncase class Comment(name: String) extends ActiveRecord { val authorId: Long lazy val author = belongsTo[User](foreignKey = “authorId”)}
  • Joining tablesClient.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 Select clients.name, clients.age, orders.price From clients inner join orders on (clients.id = orders.client_id) Where ((clients.age < 20) and (groups.price > 1000))
  • Eager loading associations Solution to N + 1 queries problem Order.includes(_.client).limit(10).map { order => order.client.name }.mkString(“n”)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))
  • Future
  • Future prospectsCompile time validation (using macro)Serialization supportWeb 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( fe conditions: Map[String, Any], e- sa foreignKey: String ttyp ) No Type-safe binding configuration
  • Serialization support パーサを個別に定義することなく、モデルを 定義するだけで済むように JSON BindForm Model XML View helper Validation MessagePackView
  • Web framework support CRUD controller Form helper scala-activerecord-play2 scala-activerecord-scalatra Code generator Controller, Model, Viewsbt generate scaffold Person name:string:required age:int scala-activerecord-play2-sbt-plugin scala-activerecord-scalatra-sbt-plugin etc..
  • Thank you