Scala ActiveRecord

6,291 views

Published on

Scala ActiveRecord

  1. 1. Scala ActiveRecordThe elegant ORM library for Scala
  2. 2. Author github.com/y-yoshinoya主にScalaとRuby on Railsの業務をやってますPlay Framework 2.0 を Beta バージョンから業務で採用を試みる DBライブラリにはSqueryl, Anorm, ScalaQuery を 採用Scala ActiveRecord はより使えるDBライブラリを求めた結果の産物
  3. 3. 概要Summary
  4. 4. Scala ActiveRecordhttps://github.com/aselab/scala-activerecordLatest version: 0.2.1License: MIT
  5. 5. 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”
  6. 6. Features Version 0.2ValidationsAssociationsTesting supportImproving query performanceScala 2.10 support
  7. 7. 背景Background
  8. 8. 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!!
  9. 9. Why created? (2)関連マッピングをまともに扱いたい・なるべく簡単に使いたいClassごとにFinder methodを定義しなければならないなどDRYに書けない本質的な処理についてだけ記述したいのにできない。操作を書きたいのであってSQLを書きたいわけではない Not DRY!!!
  10. 10. Other libraries Anorm Slick (ScalaQuery) Squeryl
  11. 11. (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...
  12. 12. (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.
  13. 13. (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.
  14. 14. 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.
  15. 15. Improvements from SquerylIterable#iterator にアクセスした時点で inTransaction するよう変更save, delete 時にデフォルトで inTransaction するように もちろん明示的に transaction もできる // auto inTransaction query.toList model.save model.delete
  16. 16. Improvements from Squeryl 関連設定ルールをCoCで結び付けられるように 関連参照時のQueryがSubQueryにならないように Eager loadingを実装 Simpler association definition rule.
  17. 17. 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)}
  18. 18. 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] }
  19. 19. Minimal example
  20. 20. 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] }
  21. 21. Create val person = Person("person1", 25) person.save trueval person = Person("person1", 25).create Person(“person1”, 25)
  22. 22. 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”))
  23. 23. UpdatePerson.find(1).foreach { p => p.name = “aaa” p.age = 19 Callback hook p.save Validations}Person.forceUpdate(_.id === 1)( _.name := “aa”, _.age := 19)
  24. 24. DeletePerson.where(_.name === “john”) .foreach(_.delete)Person.find(1) match { case Some(person) => person.delete case _ =>}Person.delete(1)
  25. 25. Query interface
  26. 26. 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
  27. 27. 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
  28. 28. 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
  29. 29. 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)
  30. 30. Limit and Offset Client.limit(10) Client.page(2, 5)Existence of objectsClient.exists(_.name like "john%") true or false
  31. 31. Selecting specific fieldsClient.select(_.name).toList List[String]Client.select(c => (c.name, c.age)).toList List[(String, Int)]
  32. 32. 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
  33. 33. Cache controlQueryからIterableに暗黙変換される際に取得したListをキャッシュとして保持 val orders = Order.where(_.age.~ > 20) // execute SQL query, and cached query orders.toList // non-execute SQL query. orders.toList
  34. 34. Validations
  35. 35. 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]
  36. 36. 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
  37. 37. 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"
  38. 38. Callbacks
  39. 39. Available hooks•beforeValidation()•beforeCreate()•afterCreate()•beforeUpdate()•afterUpdate()•beforeSave()•afterSave()•beforeDelete()•afterDelete()
  40. 40. 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
  41. 41. Associations
  42. 42. 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]}
  43. 43. 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")
  44. 44. 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
  45. 45. 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]}
  46. 46. 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”))
  47. 47. 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] }
  48. 48. 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)}
  49. 49. 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”)}
  50. 50. 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))
  51. 51. 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))
  52. 52. Future
  53. 53. Future prospectsCompile time validation (using macro)Serialization supportWeb framework support(Offers view helpers for Play 2.x and Scalatra)STI, Polymorphic Association
  54. 54. 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
  55. 55. Serialization support パーサを個別に定義することなく、モデルを 定義するだけで済むように JSON BindForm Model XML View helper Validation MessagePackView
  56. 56. 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..
  57. 57. Thank you

×