Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

Marconi Lanna

March 19, 2013
Tweet

More Decks by Marconi Lanna

Other Decks in Programming

Transcript

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

    better API.” “Using the JDBC API is tedious. We provide a simpler API.”
  2. “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
  3. MVC

  4. 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
  5. 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() } }
  6. 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() } }
  7. 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) } }
  8. Abnorm: a trait providing Active Record-like behavior almost for free.

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

    , name : String , introduced : Option[Date] , discontinued: Option[Date] ) extends ComputerActiveRecord object Computer extends ComputerDao
  10. 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) }
  11. 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) }
  12. 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) }
  13. 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} """
  14. 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.