Skip to content

Transactions

API

If you want to execute multiple statements under one transaction, use transaction function.

val players = listOf<Player>()
database.playerQueries.transaction {
  players.forEach { player ->
    database.playerQueries.insert(
      player_number = player.number,
      full_name = player.fullName
    )
  }
}

To return a value from a transaction, use the transactionWithResult function.

val players: List<Player> = database.playerQueries.transactionWithResult {
  database.playerQueries.selectAll().executeAsList()
}

Rollback

Transactions will roll back if an exception occurs anywhere in them. You can manually roll back a transaction anywhere inside of one, but if your transaction returns a value you will need to specify a value for the transaction to return.

database.playerQueries.transaction {
  players.forEach { player ->
    if (player.number == 0) rollback()
    database.playerQueries.insert(
      player_number = player.number,
      full_name = player.fullName
    )
  }
}
val numberInserted: Int = database.playerQueries.transactionWithResult {
  players.forEach { player ->
    if (player.number == 0) rollback(0)
    database.playerQueries.insert(
      player_number = player.number,
      full_name = player.fullName
    )
  }
  players.size
}

Callbacks

You can register callbacks to occur after a transaction has completed or rolled back:

database.playerQueries.transaction {
  afterRollback { log("No players were inserted.") }
  afterCommit { log("${players.size} players were inserted.") }

  players.forEach { player ->
    database.playerQueries.insert(
      player_number = player.number,
      full_name = player.fullName
    )
  }
}

Consistency of Large Queries

Using AndroidSqliteDriver, query results are read through a CursorWindow of limited size (2 MB by default). When a result does not fit in one window, AndroidSqliteDriver executes the statement again to fill the next one. A query executed outside a transaction can observe changes committed by another thread between windows, and return inconsistent rows from the database.

To guarantee that a large query reads a single consistent snapshot, execute it inside a transaction.

val players: List<Player> = database.playerQueries.transactionWithResult {
  database.playerQueries.selectAll().executeAsList()
}

Using a transaction means other writers wait until the transaction ends.

Setting a larger windowSizeBytes on AndroidSqliteDriver (API 28+) can avoid creating an additional window.

Info

Avoid INSERT, UPDATE or DELETE statements with a RETURNING clause whose result may not fit in one window. The statement is executed again for each additional window, so its changes are applied more than once. A transaction does not prevent this.