Type to search…

Transaccions

Introducció

Transaccions

Si vols executar més d’una sentència dins una transacció has d’utlitzar la funció transaction():

kotlin
db.transaction {
    val eva = db.ownerQueries.insert("Eva").executeAsOne()
    val milú = db.dogQueries.insert("Milú", null, null, eva.id).executeAsOne()
    println(milú)
}

El resultat és:

ps
Dog(id=7, name=Milú, number=null, breed=null, owner=3)

Per tornar el valor des de una transacció, utilitza la funció transactionWithResult:

kotlin
val dogs = db.transactionWithResult {
    db.dogQueries.select(5,0).executeAsList()
}
println(dogs)

Rollback

Si durant l’execució del codi es produeix una execpció, automàticamen es farà un “rollback” de la transació.

kotlin
  try {
        db.transaction {
            db.dogQueries.insert("Kokoro", null, null, null)
            throw Exception("Ups!")
        }
    } catch (e: Exception) {
        println("No hi a gossos: ${db.dogQueries.select(5, 0).executeAsList().isEmpty()}")
    }

Si vols pots fer un “rollback” de la transacció en qualsevol moment (no es produeix cap excepció):

kotlin
db.transaction {
    db.dogQueries.insert("Kokoro", null, null, null)
    rollback()
}
println("No hi a gossos: ${db.dogQueries.select(5, 0).executeAsList().isEmpty()}"

Però si la transacció retorna un valor, has de retornar un valor amb el rollback:

kotlin
val dogs: Dog? = db.transactionWithResult {
    db.dogQueries.insert("Kokoro", null, null, null)
    rollback(null)
}

Callbacks

Pots registrar “callbacks” que s’executarn un con la transacció ha finalitzar o s’ha fet un “rollback”:

kotlin
val dog: Dog? = db.transactionWithResult {

    afterRollback { println("No s'ha afegit Kokoro") }
    afterCommit { println("S'ha afegit Kokoro") }

    db.dogQueries.insert("Kokoro", null, null, null)
    rollback(null)
}

Grouping Statements

TODO

You can group multiple SQL statements together to be executed at once inside a transaction:

sql

Tipus

https://sqldelight.github.io/sqldelight/2.0.2/jvm_sqlite/types/

Cursor

Però si la llista és molt llarga, i vols processar molts elements de la llista és millor utilitzar un cursor:

kotlin
val query = db.dogQueries.select(Long.MAX_VALUE,0)
query.execute { cursor ->
  while (cursor.next().value) {
    val dog = query.mapper(cursor)
    println(dog)
  }
  QueryResult.Unit
}

La manera més fàcil d’accedir a les dades, és mitjançant la funció mapper que forma part de las classe Query<Dog>, i que transforma el registre al que apunta el cursor a un objecte Dog.

Però si vols, pots accedir als elements de registre de manera posicional:

kotlin
val query = db.dogQueries.select()
query.execute { cursor ->
  while (cursor.next().value) {
    println(cursor.getString(1))
  }
  QueryResult.Unit
}

No tens seguretat a nivell de tipus, però és molt més eficient si no necessites crear l’objecte!

I el que es evident, no cal que recorris tots els elements de la llista!

kotlin
 // Consultem la taula gossos amb màxim 2 resultats
val query = db.dogQueries.select(Long.MAX_VALUE,0)
query.execute { cursor ->
    while (cursor.next().value) {
       val dog = query.mapper(cursor)
       println(dog)
       if (dog.name == "Ketzu") {
           break
       }
    }
    QueryResult.Unit
}

Listener

Quan estas processant un select en un entorn concurrent és possible que s’afegeixin o es borrin registres.

Pots registrar un “listener”:

kotlin
val query = db.dogQueries.select(Long.MAX_VALUE,0);
query.addListener {
    app.cash.sqldelight.Query.Listener { println("queryResultsChanged") }
}

però només té sentint en un entorn concurrent …

TODO primer l’alumne ha de saber com s’executa en entorn concurrent.

Activitat

classDiagram
direction LR

class Team { 
  id integer primary key autoincrement
  name text not null unique
  location text not null
}

class Player {
  id integer primary key autoincrement
  name text not null
  country text not null
}

Player --> Team : team

{% sol %} Team.sq

sql
CREATE TABLE IF NOT EXISTS Team (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL UNIQUE,
    location TEXT NOT NULL
);

insert:
INSERT INTO Team(name,location) VALUES(?,?);

select:
SELECT * FROM Team LIMIT :limit OFFSET :offset;

selectCount:
SELECT count(*) FROM Team;

Player.sq

sql
CREATE TABLE IF NOT EXISTS Player (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    country TEXT NOT NULL,
    team INTEGER NOT NULL,
    FOREIGN KEY (team) REFERENCES Team(id)
);

insert:
INSERT INTO Player(name,country,team) VALUES (?,?, ?);

{% endsol %}

kotlin

fun insertDevelopmentData(database: Database) {

    listOf(
        Team(1, "Arsenal", "London"),
        Team(2, "Aston Villa", "Birmingham")
    ).forEach() { team ->
        database.teamQueries.insert(team.name, team.location)
    }

    listOf(
        Player(1, "David Raya", "Spain", 1),
        Player(2, "William Saliba", "France", 1),
        Player(3, "Kieran Tiernay", "Scotland", 1)
    ).forEach { player ->
        database.playerQueries.insert(player.name, player.country, player.team)
    }
}

Consultar dades

A continuació has de crear una “Screen” per mostrar tots els equips que estan a la base de dades.

1.- Crea les funcions “composables” TeamView i TeamViewList.

kotlin
    @Composable
fun TeamView(team: Team) {
    
    // ...
}

@Composable
fun TeamListView(teams: List<Team>) {
    
    // ...
}

2.- Modifica el setContent de la classe MainActivity:

kotlin
class MainActivity : ComponentActivity() {

        // ...

        val teams = db.teamQueries.select(Long.MAX_VALUE, 0).executeAsList()

        setContent {
            TeamListView(teams)
        }
}

3.- Crea la funció “composable” TeamListScreen que té la base de dades com a paràmetre:

kotlin
@Composable
fun TeamListScreen(database: Database) {
    val teams = database.teamQueries.select(Long.MAX_VALUE, 0).executeAsList()
    TeamListView(teams)
}

4.- Modifica el setContent de la classe MainActivity:

kotlin
class MainActivity : ComponentActivity() {

        // ...

        setContent {
            TeamListScreen(database)
        }
}

Afegir dades

A continuació has de crear una “Screen” per insertar un nou equip a la base de dades

1.- Completa la funció “composable” TeamInsert per afegir un equip nou.

Has d’afegir un TextField per location!

kotlin
@Composable
fun TeamInsert(handle: (Team) -> Unit) {

    val team = remember { mutableStateOf(Team(0,"","")) }

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,
        modifier = Modifier
            .fillMaxSize()
            .padding(10.dp)
    ) {
        Text(
            text = "Team",
            style = MaterialTheme.typography.titleLarge,
            color = MaterialTheme.colorScheme.primary
        )
        Spacer(modifier = Modifier.height(10.dp))
        TextField(
            value = team.value.name,
            placeholder = { Text("name") },
            onValueChange = { team.value = team.value.copy(name = it) })

        Button(
            onClick = {
                message.value = handle(team.value)
            },
            modifier = Modifier.padding(vertical = 40.dp)
        ) {
            Text("Insert")
        }
    }
}

@Preview
@Composable
fun TeamInsertPreview() {
    TeamInsert {
        println(it)
    }
}

2.- Crea la funció “composable” TeamInsertScreen.

La funció ha d’insertar el registre a la base de dades.

kotlin
@Composable
fun TeamInsertScreen(database: Database) {
    TeamInsert { team ->
        database.teamQueries.insert(team.name,team.location)
    }
}

3.- Verifica que pots insertar equips a la base de dades.

4.- Si insertes un equip amb un nom que ja existeix l’aplicació fa “crash”.

El motiu és que el nom de l’equip ha de ser únic en la taula Team.

Modifica les funcion TeamInsert i TeamInsertScreen perqué gestioni l’excepció, i mostri a l’usuari un missatge d’error dient que l’equip ja existeix.

{% sol %}

kotlin
@Composable
fun TeamInsert(handle: (Team) -> String) {

    val team = remember { mutableStateOf(Team(0, "", "")) }
    val message = remember { mutableStateOf("") }

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,
        modifier = Modifier
            .fillMaxSize()
            .padding(10.dp)
    ) {
        Text(
            text = "Team",
            style = MaterialTheme.typography.titleLarge,
            color = MaterialTheme.colorScheme.primary
        )
        Spacer(modifier = Modifier.height(10.dp))
        TextField(
            value = team.value.name,
            placeholder = { Text("name") },
            onValueChange = { team.value = team.value.copy(name = it) })
        Spacer(modifier = Modifier.height(10.dp))
        TextField(
            value = team.value.location,
            placeholder = { Text("location") },
            onValueChange = { team.value = team.value.copy(location = it) })

        Button(
            onClick = {
                message.value = handle(team.value)
            },
            modifier = Modifier.padding(vertical = 40.dp)
        ) {
            Text("Insert")
        }
        Text(
            text = message.value,
            modifier = Modifier.padding(10.dp),
            color = MaterialTheme.colorScheme.error
        )
    }
}

@Preview
@Composable
fun TeamInsertPreview() {
    // Bournemouth, Brentford
    TeamInsert {
        "Error"
    }
}

@Composable
fun TeamInsertScreen(database: Database) {

    TeamInsert { team ->
        try {
            database.teamQueries.insert(team.name, team.location)
            "Fet!"
        } catch (e: Exception) {
            e.message ?: "Error al insertar el registre"
        }
    }
}

{% endsol %}

TODO