Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Room: the blessed object mapper

Hugo Visser
November 03, 2017

Room: the blessed object mapper

Presented at #DevFestCZ

Hugo Visser

November 03, 2017
Tweet

More Decks by Hugo Visser

Other Decks in Programming

Transcript

  1. Knowing SQL • SQL Delight: everything is SQL → little

    or no abstractions, source of truth • Room → Queries and migrations are SQL • Cupboard → SQL as fallback
  2. Key points Part of Architecture Components → opinionated app architecture

    Does not support entity relations by design (e.g. no lazy loading) Database operations use SQL, no abstractions. Current version: 1.0.0-rc1 Created by Google
  3. Getting started (Java) android { defaultConfig { javaCompileOptions { annotationProcessorOptions

    { arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] } } } } dependencies { implementation "android.arch.persistence.room:runtime:1.0.0-rc1" annotationProcessor "android.arch.persistence.room:compiler:1.0.0-rc1" // 26.1.+ implements LifecyleOwner for LiveData and friends implementation 'com.android.support:appcompat-v7:26.1.0' // For LiveData & Lifecycle support implementation "android.arch.lifecycle:extensions:1.0.0-rc1" annotationProcessor "android.arch.lifecycle:compiler:1.0.0-rc1" }
  4. Getting started (Kotlin) apply plugin: 'kotlin-kapt' kapt { arguments {

    arg("room.schemaLocation", "${projectDir}/schemas") } } dependencies { implementation "android.arch.persistence.room:runtime:1.0.0-rc1" kapt "android.arch.persistence.room:compiler:1.0.0-rc1" // 26.1.+ implements LifecyleOwner for LiveData and friends implementation 'com.android.support:appcompat-v7:26.1.0' // For LiveData & Lifecycle support implementation "android.arch.lifecycle:extensions:1.0.0-rc1" kapt "android.arch.lifecycle:compiler:1.0.0-rc1" }
  5. Room main ingredients Entities → your objects DAO’s → how

    you persist and retrieve entities RoomDatabase → where everything is persisted
  6. Entities @Entity public class User { @PrimaryKey(autoGenerate = true) public

    Long id; public String firstName; public String lastName; }
  7. Entities @Entity public class User { @PrimaryKey(autoGenerate = true) public

    String id; public String firstName; public String lastName; } error: If a primary key is annotated with autoGenerate, its type must be int, Integer, long or Long.
  8. Entities @Entity public class User { @PrimaryKey public String id;

    public String firstName; public String lastName; } error: You must annotate primary keys with @NonNull. SQLite considers this a bug and Room does not allow it.
  9. Entities @Entity public class User { @PrimaryKey @NonNull private final

    String id; public String firstName; public String lastName; public User(@NonNull String id) { this.id = id; } @Ignore public User() { this.id = UUID.randomUUID().toString(); } @NonNull public String getId() { return id; } }
  10. Entities @Entity public class User { @PrimaryKey @NonNull private final

    String id; @NonNull private final String firstName; private final String lastName; public User(@NonNull String id, @NonNull String firstName, String lastName) { this.id = id; this.firstName = firstName; this.lastName = lastName; } @Ignore public User(String firstName, String lastName) { this(UUID.randomUUID().toString(), firstName, lastName); } // getters --> } @NonNull public String getId() { return id; } @NonNull public String getFirstName() { return firstName; } public String getLastName() { return lastName; }
  11. Entities @Entity data class User(@PrimaryKey val id: String = UUID.randomUUID().toString(),

    val firstName: String, val lastName: String?) val user = User(firstName = "Hugo", lastName = "Visser")
  12. Entities @Entity(tableName = "k_user", primaryKeys = arrayOf("firstName", "lastName")) data class

    User(val id: String = UUID.randomUUID().toString(), val firstName: String, val lastName: String)
  13. Entities @Entity(indices = arrayOf(Index("firstName", "lastName", unique = true))) data class

    User(@PrimaryKey val id: String = UUID.randomUUID().toString(), @ColumnInfo(index = true) val firstName: String, @ColumnInfo(name = "last_name", typeAffinity = ColumnInfo.TEXT, collate = ColumnInfo.NOCASE) val lastName: String?)
  14. Entities Annotated Java or Kotlin classes Must specify a non-null

    primary key Database representation is controlled by annotations
  15. Entities vs POJOs Distinction is made in the Room documentation

    @Entity → Object that has a table in the database POJO → any object that a result can be mapped to
  16. Data access objects (DAOs) Define methods of working with entities

    interface or abstract class marked as @Dao One or many DAOs per database
  17. DAO @Query @Dao interface UserDao { @Query("select * from User

    limit 1") fun firstUser(): User @Query("select * from User") fun allUsers(): List<User> @Query("select firstName from User") fun firstNames(): List<String> @Query("select * from User where firstName = :fn") fun findUsersByFirstName(fn: String): List<User> @Query("delete from User where lastName = :ln") fun deleteUsersWithLastName(ln: String): Int @Query("select firstName as first, lastName as last from User where lastName = :ln") fun findPersonByLastName(ln: String): List<Person> }
  18. Map result on any POJO data class Person(val first: String,

    val last:String) Room matches column names Must be able to convert column to type
  19. @Insert @Insert(onConflict = OnConflictStrategy.REPLACE) fun addUser(user: User) @Insert fun addUsers(users:

    List<User>): List<Long> @Insert fun addUserWithBooks(user: User, books: List<Book>)
  20. User with books @Entity data class User @Ignore constructor(@PrimaryKey val

    id: String, val firstName: String, val lastName: String?, @field:Ignore val books: List<Book>) { constructor(id: String = UUID.randomUUID().toString(), firstName: String, lastName: String?) : this(id, firstName, lastName, listOf()) } Needs @field:Ignore on books Primary constructor is @Ignored for Room
  21. Abstract DAO class @Dao abstract class UserBookDao { @Insert protected

    abstract fun insertSingleUser(user: User) @Insert protected abstract fun insertBooks(books: List<Book>) @Query("select * from User where id = :id") protected abstract fun findUserById(id: String) : User? @Query("select * from Book where userId = :userId") protected abstract fun findUserBooks(userId: String) : List<Book> @Transaction open fun insertUser(user: User) { insertBooks(user.books) insertSingleUser(user) } fun findUser(id: String) : User { val user = findUserById(id) return user?.copy(books = findUserBooks(id)) ?: throw IllegalArgumentException("No user with id $id") } }
  22. The database @Database(entities = arrayOf(User::class, Book::class), version = 1) abstract

    class MyDatabase : RoomDatabase() { abstract fun getUserDao(): UserDao companion object { private var instance: MyDatabase? = null fun getInstance(context: Context): MyDatabase { return instance ?: Room.databaseBuilder(context, MyDatabase::class.java, "my.db"). build().also { instance = it } } } } Annotated with @Database abstract class that extends RoomDatabase Room.databaseBuilder() → create singleton (use DI, or other means)
  23. Let’s do this! val dao = MyDatabase.getInstance(context).getUserDao() val users =

    dao.allUsers() java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
  24. It was never okay to do database ops on the

    main thread! (but we all did it)
  25. @Insert, @Delete and @Update Nothing provided by Room to run

    off the main thread Run in an executor, AsyncTask, RxJava, Kotlin co-routines For testing only: allowMainThreadQueries() on the database builder
  26. Abstract Dao strikes again abstract class BaseDao<in T> { @Insert

    protected abstract fun insertSync(vararg obj:T): LongArray suspend fun insert(vararg obj: T): LongArray { return insertSync(*obj) } } fun updateMyData() { val dao = MyDatabase.getInstance(this).getMyDao() async { dao.insert(User(firstName = "John", lastName = "Doe")) } }
  27. Observable queries Using LiveData → android.arch.lifecycle:extensions RxJava 2 → android.arch.persistence.room:rxjava2

    Delivers result async with updates @Query("select * from User") fun allUsers(): LiveData<List<User>> @Query("select * from User") fun allUsersRx(): Flowable<List<User>>
  28. Observable queries override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val dao

    = MyDatabase.getInstance(this).getUserDao() dao.allUsers().observe(this, Observer { it?.let { showUsers(it) } }) } private fun showUsers(users: List<User>) { }
  29. TypeConverter Convert a field (object) to column and vice versa

    Can be scoped from field, entity, ..., Database No default converters for things like Date or enums :(
  30. TypeConverter object MyTypeConverters { @TypeConverter @JvmStatic fun dateToLong(date: Date?) :

    Long? { return date?.time } @TypeConverter @JvmStatic fun longToDate(value: Long?) : Date? { return if (value == null) { null } else { Date(value) } } }
  31. TypeConverter @Entity data class User(@PrimaryKey val id: String, val firstName:

    String, val lastName: String, @field:TypeConverters(MyTypeConverters::class) val birthDate: Date) @Entity @TypeConverters(MyTypeConverters::class) data class User(@PrimaryKey val id: String, val firstName: String, val lastName: String, val birthDate: Date) @TypeConverters(MyTypeConverters::class) @Database(...) abstract class MyDatabase : RoomDatabase() { }
  32. Complex objects Flatten objects onto a table using @Embedded data

    class Address(val street: String, val houseNumber: String, val city: String) @Entity class User(@PrimaryKey(autoGenerate = true) val id: Long, val name: String, val address: Address)
  33. @Embedded User table will now have a street, houseNumber and

    city column too data class Address(val street: String, val houseNumber: String, val city: String) @Entity class User(@PrimaryKey(autoGenerate = true) val id: Long, val name: String, @Embedded val address: Address)
  34. Migrations When adding, changing, removing entities: schema updates Database version

    number Migrate from version x to version y Missing migration will crash your app, but save user data
  35. Creating & using migrations class UserBirthDayMigration : Migration(1, 2) {

    override fun migrate(database: SupportSQLiteDatabase) { // execute the statements required database.execSQL("ALTER TABLE User ADD COLUMN `birthDate` INTEGER") } } Room.databaseBuilder(context, MyDatabase::class.java, "mydatabase.db"). addMigrations(UserBirthDayMigration(), MyOtherMigration()). build()
  36. Migrations Tip: use exported schema definitions 1.json, 2.json etc Testing

    migrations https://d.android.com/topic/libraries/architecture/room.html#db-migration-testing fallbackToDestructiveMigration() discards data
  37. Large result sets w/ Paging Cursors (+ CursorAdapter) have their

    problems https://medium.com/google-developers/large-database-queries-on-android-cb043a e626e8 Room integrates with Paging architecture component https://developer.android.com/topic/libraries/architecture/paging.html
  38. Data sources for PagedLists // this might be a huge

    list @Query("select * from User") fun allUsers():List<User> // Generates a TiledDataSource (not observable) @Query("select * from User") fun allUsers(): TiledDataSource<User> // Generates a LivePagedListProvider (observable) that uses LIMIT and OFFSET @Query("select * from User") fun allUsers(): LivePagedListProvider<Integer, User>
  39. More stuff Foreign key support (cascading deletes, integrity checks) SupportSQLiteDatabase

    allows for alternative SQLite implementations @Relation annotation to eager fetch relations in POJOs only Testing support