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

First look at Room Persistence by Oleksiy Sazhko

First look at Room Persistence by Oleksiy Sazhko

First look at Room Persistence by Oleksiy Sazhko

GDG Ternopil

December 02, 2017
Tweet

More Decks by GDG Ternopil

Other Decks in Programming

Transcript

  1. What is Room ? The Room persistence library provides an

    abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite.
  2. Room features - Created by Google - Object mapping library

    for persisting data - One of Architecture Components - Eliminates boilerplate code - Compile-time validation for SQL query - Provide Observability - Fully support SQLite
  3. Room Components Room Database Rest of the app Data Access

    Object Entities Get Dao Get Entities Persist changes get/set field values
  4. root/build.gradle allprojects { repositories { jcenter() maven { url 'https://maven.google.com‘

    } } } app/build.gradle implementation "android.arch.persistence.room:runtime:1.0.0“ annotationProcessor "android.arch.persistence.room:compiler:1.0.0“ //RxJava2 implementation "android.arch.persistence.room:rxjava2:1.0.0“ //Testing testCompile "android.arch.persistence.room:testing:1.0.0"
  5. @Entity public class Person { @PrimaryKey(autoGenerate = true) public long

    id; public String name; public Int age; } Person.java @Entity - At least one of the field must be annotated with @PrimaryKey - Fields must be public or have getter/setter - autoGenerate = true AUTO_INCREMENT Room SQL
  6. Person.kt @Entity(tableName = "Persons") class Person(@PrimaryKey(autoGenerate = true), @ColumnInfo(name =

    "person_name") val name: String, @ColumnInfo(name = "person_age")val age: Int) { } @Entity If name is not specified, by default class name is used as the Table name and field name is used as a column name of a table id person_name person_age 1 Steve 22 … … … Table “Persons”
  7. @Entity(tableName = "Persons") class Person(@PrimaryKey(autoGenerate = true) val id: Long

    @ColumnInfo(name = "person_name") val name: String, val street: String, val city: String ) @Embedded Nested objects You do not need to create multiple tables to express one object as multiple entities Person Name Age …. Street City Person Name Age …. Address Street City Address
  8. Address.kt class Address( val street: String, val city: String )

    @Entity(tableName = "Persons") class Person(@PrimaryKey(autoGenerate = true) val id: Long @ColumnInfo(name = "person_name") val name: String, … @Embedded val address: Address @Embedded (prefix = "second_") val addressSecondary: Address) Person.kt @Embedded @Embedded annotation to represent an object that you’d like to decompose into its subfields within a table @Embedded annotation can be used on a POJO or Entity only, not for a List There is no @Entity because we don’t want to create a separate table in the database If you have multiple embedded objects you must annotate object @Embedded(prefix = “some_name”)
  9. Address.kt id person_name person_age Person.kt street city Table “Persons” second_street

    second_city 1 Steve 22 Bravil Niben Bay Anvil Tiber Septim @Embedded … … … … … … … @Embedded(prefix = “second”)
  10. open class Person( @PrimaryKey var id: Long = 0, var

    name: String = "", … var addressList: RealmList<Adress> ) : RealmObject() {} Relations Most Object Relational Mapping (ORM) libraries allow entity objects to reference each other. Room does not support entities being directly related to other entities Realm @Entity(tableName = "Persons") class Person( @ColumnInfo(name = "person_name") val name: String, … var addressList: List<Address> ) {} Room Cannot figure out how to save this field into database. You can consider adding a type converter for it. If you want to do it with Room… It is OK
  11. How should we solve this problem? - @TypeConverter It converts

    from a unknown type into a known type in terms of database types. - @Relation Is for having relation with other model class. Creates a one-to-many relation - @ForeignKey To define the relationship between tables. Creates a one-to-one, one-to-many, many-to-many relation - @Ignore Add annotation if you don’t want to include some field for the table but want to keep it in your model class
  12. class ListConverter { @TypeConverter fun fromTimestamp(value: Long?): Date? { return

    if (value == null) null else Date(value) } @TypeConverter fun dateToTimestamp(date: Date?): Long? { return date?.time ?: return null } } @TypeConverter Object TEXT, REAL INTEGER… Converts from a unknown type into a known database type @TypeConverters
  13. @Entity(foreignKeys = arrayOf(ForeignKey( entity = Person::class, parentColumns = arrayOf("id"), childColumns

    = arrayOf("personId"), onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE))) class Address(@PrimaryKey val id: Long, val personId : Long, …) @ForeignKey Person id Name Age …. It specifies that the child data is deleted Or updated when the parent data is deleted or updated - Entity may have multiple composite primary key for declaring unique values - Entity may have multiple ForeignKey @ForeignKey @Query("SELECT * FROM Person" + "INNER JOIN Address ON Person.id = Address.id WHERE Person.id= :id") Your pojo join class Return data
  14. @Dao interface PersonDao { @Insert fun insert(person: Person) ):Long @Update

    fun update(person: Person) @Delete fun delete(person: Person) @Query("SELECT * FROM Persons") fun allPerson(): List<Person> } @Insert fun insertPersons(vararg persons: Person) @Insert fun insertPersons(person1: Person, (person2: Person) @Insert fun insertPersons(persons: List<Person>) @Dao Dao interface which contains Basic functions of persistent storage If you want you can get the return value which is the new row id @DAO @Query("SELECT * FROM Persons WHERE person_name = :name“ ) fun person(name: String): Person Room only supports named bind parameter :name to avoid any confusion between the method parameters and the query bind parameters
  15. @Dao interface PersonDao { @Insert(onConflict = OnConflictStrategy.ROLLBACK) fun insert(person: Person)

    @Update(onConflict = OnConflictStrategy.REPLACE) fun update(person: Person) } @OnConflictStrategy Based on SQLite conflict specification - ABORT - FAIL - IGNORE - REPLACE - ROLLBACK OnConflictStrategy
  16. @Query @Query can return such types as: - Single entity

    - Collection of entity - Cursor - LiveData - RxJava(Single, Maybe, Flowablle ) Each @Query method is verified at compile time. It allow avoid runtime failure. @Query("SELECT * FROM Persons") fun allPerson(): List<Person>
  17. @Database(entities = arrayOf(Person::class/*, AnotherEntityType.class, AThirdEntityType.class */), version = 1) @TypeConverters(ListConverter::class)

    abstract class AppDatabase : RoomDatabase() { abstract fun personDao(): PersonDao } We have to create an abstract method for every DAO class that we create Database version array of the Entity classes You should create a database class and define some necessary parameters Initialization database @Database Add a type converter to your database class
  18. fun `getInstance`(context: Context): AppDatabase{ return instance ?: Room.databaseBuilder(context, AppDatabase::class.java, "app-database").build().also

    { instance = it } } Database instance A good practice is to use singleton approach for the database Create a database instance Database name Also we can define by builder a migration strategy or working with a database in the main stream @Database
  19. val personList = AppDatabase.getInstance(this).personDao().allPerson() java.lang.IllegalStateException: Cannot access database on the

    main thread since it may potentially lock the UI for a long period of time. We must perform operations with the database not in the main thread Don`t perform operations on the Room in the UI thread Run it in: - AsyncTask - Executor - Rx - Kotlin co-routines - RxJava2 - LiveData Observable queries:
  20. @Dao interface PersonDao { … @Query("SELECT * FROM Persons") fun

    allPerson(): List<Person> @Query("SELECT * FROM Persons") fun allPerson(): Flowable<List<Person>> @Query("SELECT * FROM Persons") fun allPerson(): LiveData<List<Person>> } RxJava 2 Android Architecture Components Observable Async queries return LiveData or RxJava’s Maybe, Single or Flowable. It allow you to get updates whenever the data changes Manually call this method if data is modified Observable
  21. Use Rx Observable fun addPerson(){ Single.fromCallable { AppDatabase.getInstance(this) .personDao()?.insert(Person(0,“Steve", 22))}

    .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()).subscribe() } fun subscribeForUpdate(){ AppDatabase.getInstance(this).personDao().allPersonObservable() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe { personList -> view.personTableUpdated(personList) } } } Observable Insert data Subscribe for update data and refresh UI update UI
  22. Creating and using migration class Migration_1_2 : Migration(1, 2) {

    override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE Persons ADD COLUMN 'birthDate' INTEGER") } } Room.databaseBuilder(this, AppDatabase::class.java, "app-database") .addMigrations(Migration_1_2, //AnotherMigration) .build() Migration Room provides an abstraction layer to ease SQLite migrations with the Migration class. Migration class defines the actions that should be performed when migrating from one version to another Identity hash String used by Room to uniquely identify every database version and stores it in the room_master_table Database versions Add migrations to builder @Database(entities = arrayOf(Person::class),version = 2) Don’t forget to change a version in your database class
  23. Creating and using migration Scenarios 1. Database scheme modified, but

    version not increased 2. Version not increased but no migration provided 3. Version increased, fallback to destructive migration enabled 4. Version increased, migration provided IllegalStateException IllegalStateException Database is cleaned Room.databaseBuilder(this, AppDatabase::class.java, "app-database") .fallbackToDestructiveMigration() .build() Data is kept Room.databaseBuilder(this, AppDatabase::class.java, "app-database") .addMigrations(Migration_1_2, //AnotherMigration) .build() Observable
  24. @Before fun initDatabase(){ database = Room .inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(), AppDatabase::class.java) .allowMainThreadQueries() .build()

    } Database initialization @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() Testing inMemoryDatabaseBuilder() - it clears the database on closing the connection to the database allowMainThreadQueries() – can allow room to execute query on UI thread
  25. Test @Test fun testPersonDao(){ val person = Person(0, "Steve", 22)

    database?.personDao()?.insert(person) … assertEquals(person.name, dbPerson?.name) assertEquals(person.age, dbPerson?.age) } @After fun closeDatabase(){ database?.close() } Testing Any data that has been added to the database will be cleared once the process is killed
  26. simple table (simple) consisting of a single integer column Room

    VS SQLite table (tracks) which describes a collection of music tracks where each contains an id, title, duration, lyrics, etc https://hackernoon.com/squeezing-performance-from-sqlite-insertions-with-room-d769512f8330
  27. - Integrated with Android Architecture Components - Eliminate boilerplate -

    Compile-time validation - Easy testing - Easy migration implementation Conclution