Introducció

Transaccions

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

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:

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

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

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ó.

  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ó):

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:

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":

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:

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:

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:

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!

 // 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":

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

Team.sq

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

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 (?,?, ?);

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.

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

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

2.- Modifica el setContent de la classe MainActivity:

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:

@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:

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!

@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.

@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.

@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"
        }
    }
}

TODO