4. DB blocks and transaction

Types of DB blocks

There are four types of DB blocks in ScalikeJDBC.

readOnly

It runs queries in read-only mode. If a non-SELECT statement is issued in this mode, ScalikeJDBC throws a runtime exception.

val count: Long = DB readOnly { implicit session =>
  sql"select count(1) from members".map(_.long(1)).single.apply().get
}

// java.sql.SQLException occurs
DB readOnly { implicit session =>
  sql"update members set name = ${"Alice"} where id = ${1}").update.apply()
}

For a non-default datasource, it needs to be written as follows:

val name: Option[String] = NamedDB('legacydb) readOnly { implicit session =>
  sql"select name from members where id = ${"name"}".map(_.string("name")).single.apply()
}

You can also make a DBSession value and use it, although you must explicitly close the resource in that case.

// Get a read-only session as an implicit DBSession type parameter
implicit val session: DBSession = DB.readOnlySession

try {
  val names: List[String] = sql"select * from members".map(_.string("name")).list.apply()
} finally { 
  session.close()
}

autoCommit

It runs queries and update operations in auto-commit mode.

val count = DB autoCommit { implicit session =>
  val updateMembers = SQL("update members set name = ? where id = ?")

  updateMembers.bind("Alice", 1).update.apply() // auto-commit
  updateMembers.bind("Bob", 2).update.apply() // auto-commit
}

NamedDB('yetanother) autoCommit { implicit session =>
  sql"insert into events values (${12345}, ${"Click"}, ${"{'user_id': 345, 'url': 'http://www.example.com/xxx'}"})"
    .update.apply()
}

Just like readOnlySession, there is autoCommitSession as well.

implicit val session: DBSession = DB.autoCommitSession()
implicit val session: DBSession = NamedDB('yetanother).autoCommitSession()

localTx

It runs queries and update operations in a single transaction enclosed in the scope of the block. The transaction is automatically rolled back if some exception is thrown from the block.

val count = DB localTx { implicit session =>
  // start of a transaction

  val updateMembers = SQL("update members set name = ? where id = ?")

  updateMembers.bind("Alice", 1).update.apply() 
  updateMembers.bind("Bob", 2).update.apply() 

  // end of a transaction
} 
// rolled back if an exception occurs

NamedDB('yetanother) localTx { implicit session =>
  SQL("insert into events ..").bind(...).update.apply()
}

Since version 2.2.0, the TxBoundary type class allows you to choose transaction boundaries other than through an exception.

import scalikejdbc._
import scala.util.Try
import scalikejdbc.TxBoundary.Try._

// The transaction is rolled back if the Try ends up to be a Failure
// as well as when an exception is thrown in the localTx block
val result: Try[Result] = DB localTx { implicit session =>
  Try { doSomeStaff() }
}

Note that localTx cannot be taken out as a DBSession because it is a transaction boundary specifier.

withinTx

It runs queries and update operations within an existing transaction. You are responsible for handling the transaction and manage the datasource by yourself.

using(DB(ConnectionPool.borrow())) { db =>
  try {
    db.begin() // start of a transaction

    val names = DB withinTx { implicit session => 
      // an IllegalStateException is thrown unless a transaction is already started
      sql"select name from members".map(_.string("name")).list.apply()
    }

    db.commit() // commit a transaction
  } catch { case e: Exception =>
    db.rollback() // an exception could be thrown
    db.rollbackIfActive() // no exceptions can be thrown
    throw e
  }
} 

Transaction management using an automatic session

I will explain how to use the AutoSession object and the NamedAutoSession class here.

Let's say you have an insert statement such as the following:

object Member {
  def create(name: String, birthday: Option[LocalDate]): Member = {
    val createdAt = DateTime.now
    val id: Long = DB localTx { implicit session =>
      sql"insert into members (name, birthday, created_at) values (${name}, ${birthday}, ${createdAt})"
        .updateAndReturnGeneratedKey.apply()
    }
    new Member(id = id, name = name, birthday = birthday, createdAt = createdAt)
  }
}

val alice: Memebr = Member.create("Alice", None)

This will work fine by itself, but the problem is that the transaction is confined in this create method.

What that means is that if you write a block like below, the Member.create would not be rolled back even when a NotFoundException was thrown. This should not be an intended behavior.

DB localTx { implicit session =>
  val member = Member.create("Alice", None)
  Group.findByName("Japan Scala Users Group") map { group =>
    GroupMember.create(group.id, member.id)
  } orElse {
    throw new NotFoundException
    // Member.create is not rolled back because it's already commit in another transaction
  }
}

Rewriting the Member.create to receive a DBSession type as an implicit parameter gets rid of the DB block.

object Member {

  def create(name: String, birthday: Option[LocalDate])(implicit session: DBSession): Member = {
    val createdAt = DateTime.now
    val id: Long = sql"insert into members (name, birthday, created_at) values (${name}, ${birthday}, ${createdAt})"
      .updateAndReturnGeneratedKey.apply()
    new Member(id = id, name = name, birthday = birthday, createdAt = createdAt)
  }
}

It is now possible to run such methods in a single transaction by implicitly passing a DBSession from outside.

DB localTx { implicit session =>
  val member = Member.create("Alice", None) // handled in the same transaction
  Group.findByName("Japan Scala Users Group") map { group =>
    GroupMember.create(group.id, member.id)
  } orElse {
    throw new NotFoundException
    // Member.create is rolled back
  }
}

However, we still have a problem. The Member.create cannot be called individually any more, but has to be placed within a DB block.

scala> Member.create("Chris", None)
<console>:18: error: could not find implicit value for parameter session: scalikejdbc.DBSession
              Member.create("Chris", None)
                           ^

scala> DB autoCommit { implicit session =>
     |   Member.create("Chris", None)
     | }
res5: Member = Member(3,Chris,None,None,2012-12-31T11:37:40.349+09:00)

AutoSession is a solution to this problem. All you need to do is to further modify the Member.create and make AutoSession object the default value of the implicit parameter.

object Member {

  def create(name: String, birthday: Option[LocalDate])
    (implicit session: DBSession = AutoSession): Member = {

    // same as before
  }
}

And voilà! You can call the Member.create without a DB block.

scala> Member.create("Chris", None)
res5: Member = Member(3,Chris,None,None,2012-12-31T11:37:40.349+09:00)

AutoSession starts a read-only session for a select statement, and an auto-commit session for update statements. Of course, a DBSession passed from outside is used over the AutoSession because it's only a default value.

Similarly, NamedAutoSession(name) can be used in the case of NamedDB.

def create(name: String)(implicit session: DBSession = NamedAutoSession('another)) = {
  // ...
}

Mapper-generator, an automatic code generator from a DB which I will explain in a later chapter, generates source code using this AutoSession.

That's it for the DB blocks and the transaction management.