[SQLDelight] Data Mapping talk, need brainstorm. I learned that SQLDelight doesn't generate entity's...
v

Vlad

over 2 years ago
[SQLDelight] Data Mapping talk, need brainstorm. I learned that SQLDelight doesn't generate entity's data classes for Join queries and just generate a result class which will contain all the fields from the query. Lets define Customer and Purchases tables:
CREATE TABLE Customer (
    id INTEGER NOT NULL PRIMARY KEY,
    name TEXT NOT NULL
);

CREATE TABLE Purchase (
    id INTEGER NOT NULL PRIMARY KEY,
    name TEXT NOT NULL,
    customerId INTEGER NOT NULL,
    FOREIGN KEY (customerId) REFERENCES Customer(id) ON DELETE CASCADE
);
And the query with LEFT JOIN assuming that it is possible for a Customer to have zero purchases:
getConsumerWithPurchases:
SELECT Customer.*, Purchase.*
FROM Customer
LEFT JOIN Purchase ON Customer.id = Purchase.customerId;
That will generate for us GetConsumerWithPurchases data class for the result set:
public data class GetConsumerWithPurchases(
  public val id: Long,
  public val name: String,
  public val id_: Long?,
  public val name_: String?,
  public val customerId: Long?,
)
1. First concern: we can see that we have
name*_*
prefixed to avoid collisions if the entities have the same names, which they often do with common names. That is kinda error prone (we will need to map it later), but I assume we can avoid that writing named SQLs and have
Purchase.name as purchaseName
2. Second concern it that as we can see that all the LEFT JOINed entity's fields are nullable. That is because it is possible that the row does not exist at all. How do we go with that on the Mapping phase? We check if
id_ != null
and then use
name_!!
,
customerId!!
peeking into the table definitions so we know it is not nullable in fact? Next day we will define that
Purchase.name
can be actually nullable and all our mappers (we will have many) will be crashing in runtime because of the
!!
? So we want to map these results row into our shiny application structure:
data class AppCustomer(
  val id: Long,
  val name: String,
  val purchases: List<Purchase>
)

data class AppPurchase(
  val id: Long,
  val name: String
)
3. Repetitive mapping concern: So we will have to create our AppPurchase instance using
GetConsumerWithPurchases.id_
and
GetConsumerWithPurchases.name_
fields via:
val purchase = Purchase(
  id = id_!!,
  name = name_!!
)
We very likely to have the Purchase fields returned as part of many other queries in the app. So we will have to do that mapping multiple times. If we just copy-paste that mapping (will be initializing data classes on the go) - we will not scale and end up into the issue mentioned in the concern 2: what if we change
Purchase.name
to nullable in the scheme. To avoid that I assume we want to create a Purchase Mapper which will take raw fields and return created Purchase object:
object PurchaseMapper {
  
  // All fields nullable
  fun fromEntity(id: Long?, name: String?): AppPurchase? {
    
    id ?: return null
    
    return AppPurchase(
      id = id,
      name = name!!
    )
  }
}
And we delegate the creating of our shiny data classes to such mappers. If schema changes we will have update the mapper only in one place. And if want to add additional field to schema - the compiler error will easily show us where forget to add the new params to the mapper call. Seems reasonable. 4. Relation to many. So we can have multiple Purchases for the Customer and we already querying them with LEFT JOIN. Before that we assumed that there are only only one or zero. Imagine we have 1 customer with 10 Purchases. The query will return to us the
List<GetConsumerWithPurchases>
with size=10. There will be all the same Customer fields and only Purchase fields will differ. And we want to pack/group that in the mapper to our single
AppCustomer
instance with the List of the purchases. Any thoughts how we do so nicely and scalable? I couldn't find any expanded talks about the 4 concern really. I see most talks cut on the point "yeaaah, we will have some issues in the mappers" and that's it. All of the KMM example apps show only simple databases even without relations. I am working in a product company and we decided to try KMP for a new app and see how it goes. I stopped with the SQLDelight mappings because I felt like that building a data stream from API to Database and delivering to the UI from scratch would take, for example, an hour for me. But working with the mappings only will multiple that hour by I dunno, up to 3x-5x depends on how many data with relations we need to fetch for a particular screen. I hope this all make any sense and not a waste. Cheers
👀 1
I'm trying to get the `DatePicker` initial selection to work correctly in my time zone. Is there som...
j

Jeff Jackson

over 2 years ago
I'm trying to get the
DatePicker
initial selection to work correctly in my time zone. Is there some way to tell the
DatePicker
what time zone to use? It appears as though it is showing the date as UTC. For example, if I set
initialSelectedDateMillis
to be
1684540800000
the picker shows
May 20, 2023
even though I'm in the Eastern Daylight timezone (UTC -4h). Here's a simple example that reproduces the issue. The picker's title is a
Text
that shows the current selection formatted for the system time zone.
@Composable
fun MainView(modifier: Modifier = Modifier) {

    Scaffold {
        var showDatePicker by remember { mutableStateOf(true) }

        Text(
            text = "Hello",
            modifier = modifier.padding(it)
        )
        if (showDatePicker) {
            // 1684540800000 milliseconds
            // UTC: Sat May 20 2023 00:00:00
            // Local: Fri May 19 2023 20:00:00
            //
            ShowDatePicker(initialSelectedDateMillis = 1684540800000) {
                showDatePicker = false
            }
        }
    }
}


@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ShowDatePicker(initialSelectedDateMillis: Long, callback: () -> Unit) {
    val datePickerState = rememberDatePickerState(
        initialSelectedDateMillis = initialSelectedDateMillis
    )

    DatePickerDialog(
        onDismissRequest =  callback,
        confirmButton = {},
    ) {
        DatePicker(
            state = datePickerState,
            title = {
                val dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
                val millis = datePickerState.selectedDateMillis ?: 0L

                Text(
                    text = Instant.ofEpochMilli(millis).atZone(TimeZone.getDefault().toZoneId()).format(dateFormatter),
                    style = MaterialTheme.typography.titleMedium,
                    modifier = Modifier.padding(20.dp)
                )
            },
            showModeToggle = false
        )
    }
}
🧵 3
Good Morning. did anybody faced an issue in Injecting Context from Android into SqlDelight Driver us...
s

Saher Al-Sous

over 1 year ago
Good Morning. did anybody faced an issue in Injecting Context from Android into SqlDelight Driver using koin for a KMP project? I have a Kotlin Multi-platform project, that has iOS, Android and Desktop in it, and I wanted to use SqlDelight to store data. however I'm using Koin as a DI. Since I have several platforms, I made the
commonMain
to
startKoin
. This file contains the expect value that will return the
SqlDriver
for each platform, and the needed implementation to communicate with the database from
commonMain
.
expect val dbModule: Module

val repoModel = module {
    single<DbRepository> {
        DbRepository(get<SqlDriver>())
    }
}

fun initKoin() = startKoin {
    modules(
        dbModule,
        repoModel
    )
}
I found out that each platform have an Issue, so I decided to focus on one platform at a time. the problem of the Android platform is that you can't get the context needed for the
AndroidSqliteDriver
.
actual val dbModule: Module = module {
    single<SqlDriver> {
        AndroidSqliteDriver(Kos.Schema, androidContext(), "Kos")
    }
}
for Android, you need the context to initialize the driver, but you have no way to inject it. since I'm starting Koin from
commonMain
, I couldn't find a way to get the android context from the
Application
class. I tried this way:
class MyApp: Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin {
            androidContext(this@MyApp)
        }
    }
}
also this way
class MyApp: Application() {
    override fun onCreate() {
        super.onCreate()
        koinApplication {
            androidContext(this@MyApp)
        }
    }
}
and even this way
class MyApp: Application() {
    override fun onCreate() {
        super.onCreate()
        loadKoinModules(
            module { 
                single { this@MyApp }
            }
        )
    }
}
and I always get an error...
Caused by: org.koin.core.error.KoinAppAlreadyStartedException: A Koin Application has already been started
Caused by: org.koin.android.error.MissingAndroidContextException: Can't resolve Context instance. Please use androidContext() function in your KoinApplication configuration.
how is it possible to get android context for SqlDelight driver within kmp using Koin?