Abnorm: A better non-ORM

Abnorm: A better non-ORM

Abnorm is a thin, concise persistence layer built on top of Play Framework's Anorm. It provides Active Record-like behaviour with minimal effort and powerful externalized SQL statements.

Talk given at the Toronto Scala Meetup, March 19, 2013.

Source code available at https://github.com/marconilanna/TorontoScalaMeetup2013March19

893174df3fa80d647663d47d37f539ac?s=128

Marconi Lanna

March 19, 2013
Tweet

Transcript

  1. Abnorm A better non-ORM Marconi Lanna

  2. Play 1 was awesome

  3. Play 2 is even better

  4. but it came with a price

  5. more verbose less flexible more formal less fun

  6. persistence

  7. Anorm “Using JDBC is a pain, but we provide a

    better API.” “Using the JDBC API is tedious. We provide a simpler API.”
  8. “I see Subversion as the most pointless project ever. Its

    slogan was ‘CVS done right’. There is no way to do CVS right.” Linus Torvalds
  9. There is no way to do JDBC right!

  10. Anorm: a simpler, better way to tedious and pain

  11. MVC

  12. Model View Controller

  13. Model View Controller Services

  14. Model View Controller Services Helpers

  15. Model View Controller Services Helpers Taglibs

  16. MVC-SHT

  17. Where’s persistence?

  18. In the model.

  19. Models are for business logic, not persistence logic.

  20. MVCP P for Persistence

  21. Separation of concerns. It works for M, V, and C.

    Why not use it for P, too?
  22. Why has persistence traditionally been shoehorned into models?

  23. Active Record Introduced by Martin Fowler Patterns of Enterprise Application

    Architecture, 2002
  24. Most programming languages do not offer an alternative way to

    implement the Active Record pattern
  25. But Scala has traits.

  26. But Scala has traits. And mix-in composition.

  27. But Scala has traits. And mix-in composition. And self types.

  28. Single Responsibility Principle

  29. Anorm Play’s Computer Database sample app Models.scala: ~180 LOC Keywords

    like introduced and discontinued appear eleven times in the class body Add a field to your class and you have eleven places to update in your code
  30. Insert def insert(computer: Computer) = { DB.withConnection { implicit connection

    => SQL( """ insert into computer values ( (select next value for computer_seq), {name}, {introduced}, {discontinued}, {company_id} ) """ ).on( 'name -> computer.name, 'introduced -> computer.introduced, 'discontinued -> computer.discontinued, 'company_id -> computer.companyId ).executeUpdate() } }
  31. Update def update(id: Long, computer: Computer) = { DB.withConnection {

    implicit connection => SQL( """ update computer set name = {name}, introduced = {introduced}, discontinued = {discontinued}, company_id = {company_id} where id = {id} """ ).on( 'id -> id, 'name -> computer.name, 'introduced -> computer.introduced, 'discontinued -> computer.discontinued, 'company_id -> computer.companyId ).executeUpdate() } }
  32. findById def findById(id: Long): Option[Computer] = { DB.withConnection { implicit

    connection => SQL("select * from computer where id = {id}").on('id -> id) .as(Computer.simple.singleOpt) } }
  33. Abnorm: a trait providing Active Record-like behavior almost for free.

  34. Abnorm: a trait providing Active Record-like behavior almost for free.

    Plus externalized SQL statements.
  35. Abnorm: a trait providing Active Record-like behavior almost for free.

    Plus externalized SQL statements. Plus convenience methods.
  36. Computer.scala case class Computer ( id : PrimaryKey = NotAssigned

    , name : String , introduced : Option[Date] , discontinued: Option[Date] ) extends ComputerActiveRecord object Computer extends ComputerDao
  37. Insert scala> Computer(NotAssigned, "PC", Some(new Date()), None).save res1: Option[models.Computer] =

    Some(Computer(2,PC,Some(...),None))
  38. findById scala> val mac = Computer(1) mac: Option[models.Computer] = Some(Computer(1,Mac,Some(...),None))

  39. Update scala> mac.copy(name="Macintosh").save res2: Option[models.Computer] = Some(Computer(1,Macintosh,Some(...),None))

  40. Retrieve and Update Computer(1).map(_.copy(name="Mac").save) res3: Option[models.Computer] = Some(Computer(1,Mac,Some(...),None))

  41. Where’s the magic?

  42. persistence/Computer.scala private[models] trait ComputerActiveRecord extends ActiveRecord[Computer] { this: Computer =>

    protected def dao = Computer private[persistence] def withId(id: PrimaryKey) = copy(id = id) } private[models] trait ComputerDao extends Dao[Computer] { this: Computer.type => protected def table = "computer" protected def parse(row: Row) = Computer( row[PrimaryKey]("id") , row[String]("name") , row[Option[Date]]("introduced") , row[Option[Date]]("discontinued") ) def byName(name: String) = selectMany('byName, 'name -> name) }
  43. persistence/Computer.scala This is all you need to implement. Everything else

    is just boilerplate. private[models] trait ComputerActiveRecord extends ActiveRecord[Computer] { this: Computer => protected def dao = Computer private[persistence] def withId(id: PrimaryKey) = copy(id = id) } private[models] trait ComputerDao extends Dao[Computer] { this: Computer.type => protected def table = "computer" protected def parse(row: Row) = Computer( row[PrimaryKey]("id") , row[String]("name") , row[Option[Date]]("introduced") , row[Option[Date]]("discontinued") ) def byName(name: String) = selectMany('byName, 'name -> name) }
  44. persistence/Computer.scala This is all you need to implement. Everything else

    is just boilerplate. Look how easy it is to implement new query methods. private[models] trait ComputerActiveRecord extends ActiveRecord[Computer] { this: Computer => protected def dao = Computer private[persistence] def withId(id: PrimaryKey) = copy(id = id) } private[models] trait ComputerDao extends Dao[Computer] { this: Computer.type => protected def table = "computer" protected def parse(row: Row) = Computer( row[PrimaryKey]("id") , row[String]("name") , row[Option[Date]]("introduced") , row[Option[Date]]("discontinued") ) def byName(name: String) = selectMany('byName, 'name -> name) }
  45. find by name scala> Computer.byName("Mac") res4: List[models.Computer] = List(Computer(1,Mac,Some(...),None))

  46. Where’s the SQL?

  47. Bring Your Own SQL

  48. But leave it on a leash outside

  49. I wanna no freaking SQL polluting my Scala code

  50. Typesafe Config (HOCON) update: """ update computer set name =

    {name} , introduced = {introduced} , discontinued = {discontinued} where id = {id} """ select: """ select * from computer """ byId: ${select}""" where id = {id} """
  51. Typesafe Config (HOCON) update: """ update computer set name =

    {name} , introduced = {introduced} , discontinued = {discontinued} where id = {id} """ select: """ select * from computer """ byId: ${select}""" where id = {id} """ You can use string concatenation. It’s not much, but it helps.
  52. code for this presentation: bit.ly/10amYVp github.com/marconilanna twitter.com/ScalaFacts “Programming is more

    than just writing code” Brian Kernighan