Skip to content

v0.14.1 Transaction handling

Compare
Choose a tag to compare
@marcgrue marcgrue released this 19 Dec 17:58
· 1 commit to main since this release

3 new transaction handling methods are introduced for SQL databases on the jvm side.

Examples shown with the sync api - could as well be async, zio or io apis

Transaction bundle

Transact multiple actions in one commit with transact(<actions...>)

transact(
  Ns.int(1).save,         
  Ns.int.insert(2, 3),    
  Ns(1).delete,           
  Ns(3).int.*(10).update, 
) // result: List(2, 30)

It's easier to write than calling .transact on all 4 actions

Ns.int(1).save.transact         
Ns.int.insert(2, 3).transact   
Ns(1).delete.transact          
Ns(3).int.*(10).update.transact

Rollback all on any error

transact(<actions..>) rolls back all actions if any fails. If for instance Type.int has a validation defined in the Data Model that requires integers to be larger than 1, then both of the following actions will be rolled back:

try {
  transact(
    Type.int.insert(4, 5),
    Type.int(1).save, // not valid, triggers rollback of both actions
  )
} catch {
  case ValidationErrors(errorMap) =>
    errorMap.head._2.head ==>
      s"""Type.int with value `1` doesn't satisfy validation:
         |_ > 2
         |""".stripMargin
}

Unit of work

When you need to transact multiple actions and also at the same time query things, you can use unitOfWork:

// Initial balance in two bank accounts
Ns.s("fromAccount").int(100).save.transact
Ns.s("toAccount").int(50).save.transact

try {
  unitOfWork {
    Ns.s_("fromAccount").int.-(200).update.transact
    Ns.s_("toAccount").int.+(200).update.transact

    // Check that fromAccount had sufficient funds
    if (Ns.s_("fromAccount").int.query.get.head < 0) {
      // Abort all transactions in this unit of work
      throw new Exception(
        "Insufficient funds in fromAccount..."
      )
    }
  }
} catch {
  case e: Exception =>
    // Do something with failure...
    e.getMessage ==> "Insufficient funds in fromAccount..."
}

// No data transacted
Ns.s.int.query.get ==> List(
  ("fromAccount", 100),
  ("toAccount", 50),
)

Savepoints

Rollback scoped actions within a unitOfWork with savepoint:

Ns.int.insert(1 to 4).transact
Ns.int(count).query.get.head ==> 4

unitOfWork {
  savepoint { sp =>
    // Delete all
    Ns.int_.delete.transact
    Ns.int(count).query.get.head ==> 0

    // Roll back delete action within savepoint scope
    sp.rollback()
    Ns.int(count).query.get.head ==> 4
  }
}

// Nothing deleted
Ns.int(count).query.get.head ==> 4

Throwing an exception instead of calling sp.rollback() has the same effect.

Using savepoints can be a convenient way of rolling back only parts of a larger body of transactions/unitOfWork. Savepoints can even be nested (see tests).