Android Room Journey #1: Complex model storing (DB relations)

Adam Styrc
TechTalks@Vattenfall
7 min readDec 12, 2019

There are 3 ways of saving persistent data on Android:

  • Shared Preferences
  • File
  • Database

In many cases saving to a Shared Preferences is sufficient. You can not only keep the primitive values like Integer, Boolean and String but also serialize your data objects to a JSON and keep them as a String. Then, when relaunching the app, you could use Gson().fromJson() to restore your data.

In the InCharge app, this solution was used for a long time. It’s very simple, clear, you find out what is happening easily. There are no problems… until you reach the Shared Preferences limitations 🙄🙄 which are:

max String length = Integer.MAX_VALUE (=2147483647)

some people claim it’s 1.42 MB (1428.51KB)

per key.

At some point, we started to face this limitation. We store long lists of complex objects and these JSONs started to swell. How to transition to a database? Here we go!

Why Android Room?

There are plenty of approaches to database topics on Android, from pure SQLite through Content Providers, many ORM like e.g. GreenDao, to even non-SQLite DBs like Realm. I’ve decided on yet youngest tool which is Android Room. My motivation to use Room was:

  • the API looks clearer than all others ORM I’ve used in the past
  • there is advanced support of relationships between models
  • autoboxing data into LiveData which is very comfortable to use on a UI
  • support of RxJava
  • and finally, it’s a most modern tool, made & recommended by Google

Now I’d like to share the challenges I faced while implementing Room and how I coped with them.

Complex models challenge

The complex model I mention at the very beginning of this article that started to grow is a list of user’s electric car chargings. The model of the charging has lots of data and the charging takes place every few days if not every day. That’s the reason the data’s been swelling up.

There are domain models that are used across the whole project, but the way they exist is not adjusted for Room entities. Thus, firstly I had to create corresponding database models and here I faced 2 problems:

  1. Relations — model of charging session has a reference to charging station where it occurred and the charging station has a relation to charging points. Moreover charging session points out on which charging point the charging took place. This is how simplified data models related to ChargingSession looks like:

So the question emerges — how does Room handle these relations mapping?

2. Models consists of enum fields that I’d like to keep for data integrity

Relations mapping

In this article, I’m going to skip full Room implementation as there are a lot of great articles on medium or just the official code lab:

https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/#0

and focus on Entities. Starting from the bottom — ChargingPoint, we need to map it to a database entity DatabaseChargingPoint (some people might name it ChargingPointDto or ChargingPointDbo).

This will be the structure of data stored and read in a Database.

@Entity(tableName = "charging_point")
data class DatabaseChargingPoint(
@PrimaryKey var id: Long = -1,
@ForeignKey(
entity = DatabaseChargingStation::class,
parentColumns = ["id"],
childColumns = ["chargingStationId"],
onDelete = ForeignKey.CASCADE)
var chargingStationId: Long? = null,
var power: Watts = 0,
var status: String = ""
) {

companion object {
fun fromDomainObject(chargingPoint: ChargingPoint, chargingStationId: String) =
DatabaseChargingPoint(
id = chargingPoint.id!!,
chargingStationId = chargingStationId
status = chargingPoint.status,
power = chargingPoint.power
)
}

fun toDomainObject() =
ChargingPoint(
id = id,
power = power,
status = status
)
}

We connected DatabaseChargingPoint with a DatabaseChargingSession with @ForeignKey annotation so whenever a database object of charging station gets deleted, our charging points will be deleted as well.

The default values of parameters are recommended to omit the default constructor required by @Entity annotation.

I also added 2 methods for mapping from domain object ChargingPoint to DatabaseChargingPoint for storing in the database (fromDomainObject()), and in opposite direction for reading (toDomainObject()).

Let’s continue up the model tree:

@Entity(tableName = "charging_station")
data class DatabaseChargingStation(
@PrimaryKey var id: Long = -1,
@Embedded var location: Location = Location()
) {


companion object {
fun fromDomainObject(chargingStation: ChargingStation) =
DatabaseChargingStation(
id = chargingStation.id,
location = Location.fromDomainObject(chargingStation.location)
)
}

fun toDomainObject() =
ChargingStation(
id = id,
location = location.toDomainObject(),
)
}
data class Location(
var address: String? = null,
var latitude: Double? = null,
var longitude: Double? = null
) {

companion object {
fun fromDomainObject(location: ChargingStation.Location) = Location(
address = location.address,
latitude = location.coordinates?.latitude,
longitude = location.coordinates?.longitude)
}

fun toDomainObject(): ChargingStation.Location {
var coordinates: Coordinates? = null
if (latitude != null && longitude != null) {
coordinates = Coordinates(latitude!!, longitude!!)
}

return ChargingStation.Location(
address = address,
coordinates = coordinates
)
}
}

DatabaseChargingStation creation was quite similar but @Embedded annotation was used to flatten the encapsulated structure on a database table. The address, latitude, and longitude will be stored on the same level as e.g. is of the charging station.

And last Entity to model:

@Entity(tableName = "charging_session")
data class DatabaseChargingSession (
@PrimaryKey var id: Long = -1,
var startDate: Date? = null,
var endDate: Date? = null,
var chargingPointId: String? = null,
var chargingStationId: Long? = null,
) {

companion object {
fun fromDomainObject(chargingSession: ChargingSession) =
DatabaseChargingSession(
id = chargingSession.id!!,
startDate = chargingSession.startDate,
endDate = chargingSession.endDate,
chargingPointId = chargingSession.chargingPoint?.id,
chargingStationId = chargingSession.chargingStation?.id
)
}
}

Now it’s time to add a Dao classes. First thought would be to implement DAOs in a standard way:

@Dao
interface ChargingPointDao {

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun addChargingPoint(chargingPoint: DatabaseChargingPoint)
@Query("SELECT * FROM charging_point WHERE id=:id}
fun getChargingPoint(id: Long): DatabaseChargingPoint
@Dao
interface ChargingStationsDao {
//like above
}
@Dao
interface ChargingSessionsDao {

@Transaction
@Query("SELECT * FROM charging_session ORDER BY id DESC")
fun getChargingSessions()
: List<DatabaseChargingSession>

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun addChargingSession(chargingSession: DatabaseChargingSession)
}

All DAOs looks normal, like in Android Room Codelabs, but will it work the way we wanted? The answer is it depends 🙃 When building the final domain object of charging sessions we’d have to manually use all the DAOs and pull required DatabaseChargingStation and DatabaseChargingPoint… but this would be ugly. What we could actually do is to force Room to work for us!

data class DatabaseChargingStationDetails(
@Embedded var databaseChargingStation: DatabaseChargingStation,
@Relation(
parentColumn = "id",
entityColumn = "chargingStationId",
entity = DatabaseChargingPoint::class
)
var databaseChargingPoints: List<DatabaseChargingPoint>
) {
fun toDomainObject(): ChargingStation? {
val chargingStation = databaseChargingStation.toDto()
chargingStation.chargingPoints = databaseChargingPoints.map { it.toDto() }
return chargingStation
}
}
data class DatabaseChargingSessionDetails(
@Embedded var chargingSessionModel: DatabaseChargingSession,
@Relation(
parentColumn = "chargingPointId",
entityColumn = "id",
entity = DatabaseChargingPoint::class)
var databaseChargingPoint: DatabaseChargingPoint? = null,
@Relation(
parentColumn = "chargingStationId",
entityColumn = "id",
entity = DatabaseChargingStation::class)
var databaseChargingStation: DatabaseChargingStationDetails? = null
) {
fun toDomainObject() =
chargingSessionModel.toDomainObject().apply {
chargingPoint = databaseChargingPoint?.toDomainObject()
chargingStation = databaseChargingStation?.toDomainObject()
}
}

and now our DAOs will be even shorter and already pull charging sessions objects that contain charging point and charging station that contains it’s all charging points :) All the magic is obtained with @Relation annotation.

@Dao
interface ChargingPointDao {

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun addChargingPoint(chargingPoint: DatabaseChargingPoint)
}
@Dao
interface ChargingStationsDao {

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun addChargingStation(databaseChargingStation: DatabaseChargingStation)
}
@Dao
interface ChargingSessionsDao {

@Transaction
@Query("SELECT * FROM charging_session ORDER BY id DESC")
fun getChargingSessionsWithChargingStationAndPoint()
: List<DatabaseChargingSessionDetails>

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun addChargingSession(chargingSession: DatabaseChargingSession)
}

In addition, we can replace the returned type from List<> to LiveData<List<>> so getting the data will be async!

@Transaction
@Query("SELECT * FROM charging_session ORDER BY id DESC")
fun getChargingSessionsWithChargingStationAndPoint()
: LiveData<List<DatabaseChargingSessionDetails>>

Transforming to domain objects

Wrapping data access in a layer is generally a good idea. We could access domain models from a database source via some Repository class that could have e.g. fetch() method. From now on we could forget that we have the database models:

interface ChargingSessionsRepository {

fun fetchChargingSessions(): LiveData<List<ChargingSession>>

fun saveChargingSessions(chargingSessions: List<ChargingSession>)
}
class DatabaseChargingSessionsRepository internal constructor(
private val database: InchargeRoomDatabase
) : ChargingSessionsRepository {

override fun fetchChargingSessions() = Transformations.map(
database.chargingSessionsDao()
.getChargingSessionsWithChargingStationAndPoint()
) { databaseChargingSession ->
databaseChargingSession.map {
it
.chargingSessionModel.toDomainObject()
}
}

Transformations.map() is used in order to transition from database to domain models 🤓

Saving the data

We did some nice work obtaining data from the database but so far it’s empty and we’ve not put anything yet 🙊

Here is the missing method of DatabaseChargingSessionsRepository class.

override fun saveChargingSessions(chargingSessions: List<ChargingSession>) {
database.runInTransaction {
chargingSessions.forEach {
try {
val chargingSessionModel = DatabaseChargingSession.fromDomainObject(it)

it.chargingStation?.let { chargingStation ->
database.chargingStationsDao().addChargingStation(
DatabaseChargingStation.fromDto(chargingStation)
)

chargingStation.chargingPoints.forEach { chargingPoint ->
database.chargingPointsDao().addChargingPoint(
DatabaseChargingPoint.fromDto(chargingPoint, chargingStation.id)
)

chargingPoint.connectors.forEach { connector ->
database.chargingConnectorDao().addChargingConnector(
DatabaseChargingConnector.fromDto(connector, chargingPoint.id)
)
}
}
}

database.chargingSessionsDao().addChargingSession(chargingSessionModel)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}

This is the point where I reached. Probably I will next play with wrapping the saving process into RxJava and throwing it on a separate thread. 🤔

Conclusions

Maintaining complex models and relations between them is definitely possible with Android Room. TBH our models are way more complicated as there are more things embedded and we have nested relations in relations. For clarity and brevity, I simplified it — however, the way to achieve it remains the same.😊

Transitioning to from Shared Preferences to Android Room took me 2 days and the first implementation worked surprising great from the scratch. Don’t fear to start using it! 🤘🤘🤘

Recommended links:

https://medium.com/swlh/android-room-persistence-library-relations-in-a-nested-one-to-many-relationship-f2fe21c9e1ad

https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/#0

--

--