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

Room: an SQLite object mapping library

Room: an SQLite object mapping library

Handling an SQLite database in Android implies challenges like a lot of boilerplate code, database operations on the main thread, queries checked at runtime and especially unmaintainable code. At Google I/O 2017, the Android team launched Room, an SQLite object mapper, that provides a set of components and features that will solve all these challenges and will help us, the developers, to have a better experience when using a database in our app.

The presentation will include examples about how to use the main components from Room (@Entity, @Dao, @Database), how to handle the relations between the entities, how to use the (observable) queries and not run database operations on the main thread. Also, we will discover more details about the migration support, compile time query verification and what’s happening behind the scenes.

Magda Miu

June 25, 2018
Tweet

More Decks by Magda Miu

Other Decks in Programming

Transcript

  1. If you would have the possibility to change something in

    the Android development what will it be that thing?
  2. ‘’ I don’t want to deal with technical things. I

    just want to deliver the project. yiğit boyar - Architecture Components – Behind the Scenes
  3. ‘’ I don’t want to deal with technical things. I

    just want to deliver the project. yiğit boyar - Architecture Components – Behind the Scenes
  4. public class DatabaseHandler extends SQLiteOpenHelper { // All Static variables

    // Database Version private static final int DATABASE_VERSION = 1; // Database Name private static final String DATABASE_NAME = "CountriesManager"; // Countries table name private static final String TABLE_Countries = "Country"; // Countries Table Columns names private static final String KEY_ID = "id"; private static final String KEY_NAME = "name"; private static final String KEY_TOWN = "town";
  5. public DatabaseHandler(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } //

    Creating Tables @Override public void onCreate(SQLiteDatabase db) { String CREATE_Countries_TABLE = "CREATE TABLE " + TABLE_Countries + "(" + KEY_ID + " INTEGER PRIMARY KEY," + KEY_NAME + " TEXT," + KEY_TOWN + " TEXT" + ")"; db.execSQL(CREATE_Countries_TABLE); }
  6. public void addCountry(Country country) { SQLiteDatabase db = this.getWritableDatabase(); db.beginTransaction();

    try { ContentValues values = new ContentValues(); values.put(KEY_NAME, country.getName()); // Country Name values.put(KEY_TOWN, country.getTown()); // Country Town db.insert(TABLE_Countries, null, values); db.setTransactionSuccessful(); } catch (Exception e) { Log.d(TAG, "Error while trying to add country to database"); } finally { db.endTransaction(); } }
  7. Country getCountry(int id) { SQLiteDatabase db = this.getReadableDatabase(); Country country

    = null; Cursor cursor = db.query(TABLE_Countries, new String[]{KEY_ID, KEY_NAME, KEY_TOWN}, KEY_ID + "=?", new String[]{String.valueOf(id)}, null, null, null, null); try { if (cursor != null) { cursor.moveToFirst(); country = new Country(Integer.parseInt(cursor.getString(0)), cursor.getString(1), cursor.getString(2)); } } catch (Exception e) { Log.d(TAG, "Error while trying to get countries from database"); } finally { if (cursor != null && !cursor.isClosed()) cursor.close();
  8. SQLite challenges ❏ Boilerplate code ❏ SQL queries checked at

    runtime ❏ Database operations on the main thread
  9. SQLite challenges ❏ Boilerplate code ❏ SQL queries checked at

    runtime ❏ Database operations on the main thread ❏ Unmaintainable code
  10. SQLite challenges ❏ Boilerplate code ❏ SQL queries checked at

    runtime ❏ Database operations on the main thread ❏ Unmaintainable code ❏ Untestable code
  11. YES NO Compile time query verification x Migration support x

    Java Query Builder API x SQL Completion in Android Studio x Relationships x Architecture Components - Behind the Scenes by Yiğit Boyar
  12. public class Company { private int companyId; private String name;

    Bitmap picture; } Company id integer name text
  13. @Entity(tableName = "Company") public class Company { @PrimaryKey(autoGenerate = true)

    @ColumnInfo(name = "id") private int companyId; @ColumnInfo(name = "name") private String name; @Ignore Bitmap picture; Company id integer name text
  14. @Entity(tableName = "Company") public class Company { @PrimaryKey(autoGenerate = true)

    @ColumnInfo(name = "id") private int companyId; @ColumnInfo(name = "name") private String name; @Ignore Bitmap picture; Company id integer name text
  15. @TypeConverter public class DateConverter { @TypeConverter public static Date toDate(Long

    timestamp) { return timestamp == null ? null : new Date(timestamp); } @TypeConverter public static Long toTimestamp(Date date) { return date == null ? null : date.getTime(); } } Company id integer name text date integer
  16. @TypeConverter @ColumnInfo(name = "date_updated") @TypeConverters(DateConverter.class) private Date itemUpdatedDate; ------------------------------------------------- @Dao

    @TypeConverters(DateConverter.class) public interface CompanyDao ------------------------------------------------- @Database(entities = {Company.class}, version = 1) @TypeConverters(DateConverter.class) public abstract class AppDatabase extends RoomDatabase
  17. @TypeConverter @ColumnInfo(name = "date_updated") @TypeConverters(DateConverter.class) private Date itemUpdatedDate; ------------------------------------------------- @Dao

    @TypeConverters(DateConverter.class) public interface CompanyDao ------------------------------------------------- @Database(entities = {Company.class}, version = 1) @TypeConverters(DateConverter.class) public abstract class AppDatabase extends RoomDatabase
  18. @TypeConverter @ColumnInfo(name = "date_updated") @TypeConverters(DateConverter.class) private Date itemUpdatedDate; ------------------------------------------------- @Dao

    @TypeConverters(DateConverter.class) public interface CompanyDao ------------------------------------------------- @Database(entities = {Company.class}, version = 1) @TypeConverters(DateConverter.class) public abstract class AppDatabase extends RoomDatabase
  19. @Dao public interface CompanyDao { @Query("SELECT * FROM Company") LiveData<List<Company>>

    getAllCountries(); @Insert void insertCompany(Company company); @Update void updateCompany(Company company); @Delete void deleteCompany(Company company); }
  20. @Dao public interface CompanyDao { @Query("SELECT * FROM Company") List<Company>

    getAllCompanies(); @Insert void insertCompany(Company company); @Update void updateCompany(Company company); @Delete void deleteCompany(Company company); }
  21. @Dao public interface CompanyDao { @Query("SELECT * FROM Companies") List<Company>

    getAllCompanies(); @Insert void insertCompany(Company company); @Update void updateCompany(Company company); @Delete void deleteCompany(Company company); }
  22. @Dao public interface CompanyDao { @Query("SELECT * FROM Companies") List<Company>

    getAllCompanies(); @Insert void insertCompany(Company company); @Update void updateCompany(Company company); @Delete void deleteCompany(Company company); }
  23. public class DepartmentDao_Impl implements DepartmentDao { private final RoomDatabase __db;

    public void insertAll(List<Department> departments) { this.__db.beginTransaction(); try { this.__insertionAdapterOfDepartment.insert(departments); this.__db.setTransactionSuccessful(); } finally { this.__db.endTransaction(); } } } Behind the scenes
  24. @Database(entities = {Company.class}, version = 1) public abstract class AppDatabase

    extends RoomDatabase { private static AppDatabase INSTANCE; public abstract CompanyDao companyDao(); public static AppDatabase getAppDatabase(Context context) { if (INSTANCE == null) { INSTANCE = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "company-db") .build(); } return INSTANCE;
  25. @Database(entities = {Company.class}, version = 1) public abstract class AppDatabase

    extends RoomDatabase { private static AppDatabase INSTANCE; public abstract CompanyDao companyDao(); public static AppDatabase getAppDatabase(Context context) { if (INSTANCE == null) { INSTANCE = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "company-db") .build(); } return INSTANCE;
  26. @Database(entities = {Company.class}, version = 1) public abstract class AppDatabase

    extends RoomDatabase { private static AppDatabase INSTANCE; public abstract CompanyDao companyDao(); public static AppDatabase getAppDatabase(Context context) { if (INSTANCE == null) { INSTANCE = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "company-db") .build(); } return INSTANCE;
  27. @Database(entities = {Company.class}, version = 1) public abstract class AppDatabase

    extends RoomDatabase { private static AppDatabase INSTANCE; public abstract CompanyDao companyDao(); public static AppDatabase getAppDatabase(Context context) { if (INSTANCE == null) { INSTANCE = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "company-db") .build(); } return INSTANCE;
  28. @Database(entities = {Company.class}, version = 1) public abstract class AppDatabase

    extends RoomDatabase { private static AppDatabase INSTANCE; public abstract CompanyDao companyDao(); public static AppDatabase getAppDatabase(Context context) { if (INSTANCE == null) { INSTANCE = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "company-db") .build(); } return INSTANCE;
  29. @Entity(tableName = "Company") public class Company { @PrimaryKey(autoGenerate = true)

    @ColumnInfo(name = "id") private int companyId; private int directorId; private Employee director; public Employee getDirector() { if(director == null) { director = getEmployee(employeeId) } return director; }
  30. @Embedded (Nested objects) @Entity(tableName = "Company") public class Company {

    @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") private int companyId; @ColumnInfo(name = "name") private String name; @ColumnInfo(name = "date_updated") @TypeConverters(DateConverter.class) private Date itemUpdatedDate; @Embedded private Location location; @Embedded private Location headLocation;
  31. @Embedded (Nested objects) @Entity(tableName = "Company") public class Company {

    @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") private int companyId; @ColumnInfo(name = "name") private String name; @ColumnInfo(name = "date_updated") @TypeConverters(DateConverter.class) private Date itemUpdatedDate; @Embedded private Location location; @Embedded(prefix = "hq_") private Location headLocation;
  32. Employee id integer name text company_id integer Company id integer

    name text date integer latitude real longitude real hq_latitude real hq_longitude real 1 M
  33. @ForeignKey @Entity(foreignKeys = @ForeignKey(entity = Company.class, parentColumns = "id", childColumns

    = "company_id", onDelete = ForeignKey.NO_ACTION)) public class Employee { @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") private int employeeId; @ColumnInfo(name = "name") private String name; @ColumnInfo(name = "company_id") private int companyId;
  34. @Relation @Entity public class Department { @PrimaryKey private int id;

    private int companyId; private String name; … Department id integer name text
  35. @Relation public class CompanyAndAllDepartments { @Embedded public Company company; @Relation(parentColumn

    = "id", entityColumn = "companyId", entity = Department.class) public List<Department> departments; … --------------------------------------------------
  36. @Relation public class CompanyAndAllDepartments { @Embedded public Company company; @Relation(parentColumn

    = "id", entityColumn = "companyId", entity = Department.class) public List<Department> departments; … @Dao public interface CompanyDepartmentsDao { @Query("SELECT * FROM Company WHERE id = :companyId") CompanyAndAllDepartments loadCompanyAllDepartments(long companyId); … --------------------------------------------------
  37. @Insert @Update @Delete @Insert(onConflict = OnConflictStrategy.ABORT) void insertCompany(Company company); @Insert(onConflict

    = OnConflictStrategy.REPLACE) void insertAll(List<Company> companies); @Insert(onConflict = OnConflictStrategy.REPLACE) void insertAll(Company... companies); ------------------------------------------------
  38. @Insert @Update @Delete @Insert(onConflict = OnConflictStrategy.REPLACE) void insertCompany(Company company); @Insert(onConflict

    = OnConflictStrategy.REPLACE) void insertAll(List<Company> companies); @Insert(onConflict = OnConflictStrategy.REPLACE) void insertAll(Company... companies); @Update void updateCompany(Company company); @Update void updateCompanies(Company... company); ------------------------------------------------ ------------------------------------------------
  39. @Insert @Update @Delete @Insert(onConflict = OnConflictStrategy.REPLACE) void insertCompany(Company company); @Insert(onConflict

    = OnConflictStrategy.REPLACE) void insertAll(List<Company> companies); @Insert(onConflict = OnConflictStrategy.REPLACE) void insertAll(Company... companies); @Update void updateCompany(Company company); @Update void updateCompanies(Company... company); ------------------------------------------------ @Delete void deleteCompany(Company company); @Delete void deleteCompanies(Company... company); ------------------------------------------------
  40. @Query* @Query("SELECT * FROM Company WHERE name = :companyName") List<Company>

    getCompanies(String companyName); @Query("SELECT * FROM Company") LiveData<List<Company>> getAllCompanies(); @Query("SELECT * FROM Company") Flowable<List<Company>> getAllCompanies(); @RawQuery(observedEntities = Employee.class) LiveData<List<Employee>> getAllEmployeesWithLimit(SupportSQLiteQuery query); java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
  41. @Transaction @Dao public abstract class DepartmentDao { @Insert public abstract

    void insert(Department product); @Delete public abstract void delete(Department product); @Transaction public void insertAndDeleteInTransaction(Department newDepartment, Department oldDepartment) { // Anything inside this method runs in a single transaction. insert(newDepartment); delete(oldDepartment); } }
  42. Company id integer name text date integer latitude real longitude

    real hq_latitude real hq_longitude real Version 1
  43. Company id integer name text date integer latitude real longitude

    real hq_latitude real hq_longitude real ref_no text Version 2
  44. // Upgrading database @Override public void onUpgrade(SQLiteDatabase db, int oldVersion,

    int newVersion) { // oldVersion -> newVersion } “CLASSICAL” APPRO ACH
  45. final Migration MIGRATION_X_Y = new Migration(X, Y) { @Override public

    void migrate(SupportSQLiteDatabase database) { // your code here } };
  46. final Migration MIGRATION_X_Y = new Migration(X, Y) { @Override public

    void migrate(SupportSQLiteDatabase database) { // your code here } }; RO O M APPRO ACH
  47. static final Migration MIGRATION_1_2 = new Migration(1, 2) { @Override

    public void migrate(SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE Company " + " ADD COLUMN ref_no TEXT"); } };
  48. public static AppDatabase getAppDatabase(Context context) { if (INSTANCE == null)

    { INSTANCE = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME) .addMigrations(MIGRATION_1_2) .build(); } return INSTANCE; }
  49. public static AppDatabase getAppDatabase(Context context) { if (INSTANCE == null)

    { INSTANCE = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME) .addMigrations(MIGRATION_1_2) .build(); } return INSTANCE; }
  50. /** * You can set annotation processor argument (room.schemaLocation) to

    tell Room to * export the schema into a folder. Even though it is not mandatory, it is a good * practice to have version history in your codebase and you should commit that file * into your version control system (but don't ship it with your app!). * */ //export schema javaCompileOptions { annotationProcessorOptions { arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] } }
  51. /** * Room will throw an IllegalStateException if you don’t

    provide a Migration. * */ if (INSTANCE == null) { INSTANCE = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "company-db") .fallbackToDestructiveMigration() .build(); }
  52. /** * Room will throw an IllegalStateException if you don’t

    provide a Migration. * */ if (INSTANCE == null) { INSTANCE = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "company-db") .fallbackToDestructiveMigration() .build(); }
  53. /** * Room will throw an IllegalStateException if you don’t

    provide a Migration. * */ if (INSTANCE == null) { INSTANCE = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "company-db") .fallbackToDestructiveMigrationFrom(int... startVersions) .build(); }
  54. @RunWith(AndroidJUnit4.class) public class SimpleEntityReadWriteTest { private CompanyDao mCompanyDao; private AppDatabase

    mDb; @Before public void createDb() { Context context = InstrumentationRegistry.getTargetContext(); mDb = Room.inMemoryDatabaseBuilder(context, AppDatabase.class).build(); mCompanyDao = mDb.companyDao(); } @After public void closeDb() throws IOException { mDb.close(); }
  55. @RunWith(AndroidJUnit4.class) public class SimpleEntityReadWriteTest { private CompanyDao mCompanyDao; private AppDatabase

    mDb; @Before public void createDb() { Context context = InstrumentationRegistry.getTargetContext(); mDb = Room.inMemoryDatabaseBuilder(context, AppDatabase.class).build(); mCompanyDao = mDb.companyDao(); } @After public void closeDb() throws IOException { mDb.close(); }
  56. @Test public void writeCompanyAndReadInList() throws Exception { Company company =

    TestUtil.createCompany("DevTalksJr"); mCompanyDao.insertCompany(company); List<Company> byName = mCompanyDao.getCompanies("DevTalksJr"); assertEquals("Should be equal", byName.get(0), company); } // If we use LiveData/RxJava we could use InstantTaskExecutorRule @Rule public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
  57. // mockito public class CompanyRepositoryTest { private CompanyDao companyDao =

    mock(CompanyDao.class); private CompanyRepository companyRepository; @Before private void setUp() throws Exception { companyRepository = new CompanyRepository(companyDao); } } // espresso @Rule public CountingTaskExecutorRule mCountingTaskExecutorRule = new CountingTaskExecutorRule();
  58. // testing migration @Rule public MigrationTestHelper testHelper = new MigrationTestHelper(

    InstrumentationRegistry.getInstrumentation(), <your_database_class>.class.getCanonicalName(), new FrameworkSQLiteOpenHelperFactory()); // Create the database with version 2 SupportSQLiteDatabase db = testHelper.createDatabase(TEST_DB_NAME, 2); // Validate the migration db = testHelper.runMigrationsAndValidate(TEST_DB_NAME, 4, validateDroppedTables, MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4);
  59. onDelete/onUpdate = ForeignKey.CASCADE int CASCADE • A "CASCADE" action propagates

    the delete or update operation on the parent key to each dependent child key. int NO_ACTION //default • When a parent key is modified or deleted from the database, no special action is taken. int RESTRICT • The RESTRICT action means that the application is prohibited from deleting (for onDelete()) or modifying (for onUpdate()) a parent key when there exists one or more child keys mapped to it. int SET_DEFAULT • The "SET DEFAULT" actions are similar to SET_NULL, except that each of the child key columns is set to contain the columns default value instead of NULL. OnConflictStrategy int REPLACE = 1; OnConflict strategy constant to replace the old data and continue the transaction. int ROLLBACK = 2; OnConflict strategy constant to rollback the transaction. int ABORT = 3; //default OnConflict strategy constant to abort the transaction. int FAIL = 4 OnConflict strategy constant to fail the transaction. int IGNORE = 5; OnConflict strategy constant to ignore the conflict.
  60. Behind the scenes... 1. SQLite supports triggers that fire whenever

    a DELETE, UPDATE or INSERT happens in a table. 2. Room creates an InvalidationTracker that uses Observers that track whenever something has changed in the observed tables. 3. Both LiveData and Flowable queries rely on the InvalidationTracker.Observer#onInvalidated notification. When this is received, it triggers a re-query. Flowables -> Flowable#distinctUntilChanged LiveData -> MediatorLiveData
  61. Room advantages ❏ Less boilerplate code ❏ SQL queries checked

    at compile time ❏ No database operations on the main thread ❏ Maintainable code ❏ Migration support
  62. ▣ Official docs: https://developer.android.com/topic/libraries/a rchitecture/room.html ▣ Google code samples and

    Youtube channel: https://github.com/googlesamples/android-ar chitecture-components ▣ CommonsWare: https://commonsware.com/AndroidArch/ ▣ Slides from: https://www.slidescarnival.com/ and gifs from: https://gifer.com Resources