Room Persistence Library

By Ali Mohammadi Nasrabadi

I Hate SQLite API

Non Maintainable Codes

Run Time Exception

Boiler Plate Codes

App Failures

Migrations

Testing

ORM

Object Relational Mapping

  • Architecture Components

  • Room Persistence Library

  • Compare ORMs

Architecture Components

App Architecture


Maintainable
Testable

  • LifeCycle

  • LiveData

  • ViweModel

  • Room

  • Paging

Room

@Entity

Table

data access object

database

@Dao

@Database

@Entity

@Entity(tableName = "product")
public class Product {

    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    public int mId;

    public String name;

    public int price;

    @Ignore
    public Bitmap picture;

}
@Entity(tableName = "product")
public class Product {

    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    private int mId;

    private String name;

    private int price;

    @Ignore
    private Bitmap picture;

    public Product(int id, String name, int price    
            , Bitmap picture) {

        mId = id;
        this.name = name;
        this.price = price;
        this.picture = picture;
    }

}

@Dao

@Dao
public interface ProductDao {

    @Insert
    void insertProducts(Product... product);

    @Update
    void updateProducts(Product... product);

    @Delete
    void deleteProducts(Product... product);

    @Query("SELECT * FROM product")
    List<Product> getAllProducts();

}
@Query("SELECT * FROM product WHERE id IS :id")
    Product getProductWithId(int id);
@Query("SELECT * FROM product WHERE name IS (:names)")
    List<Product> getProductsWithName(List<String> names);
public class ProductMainInfo {

    public int price;
    public String name;

}
@Query("SELECT name,price FROM product WHERE id IS :id")
    ProductMainInfo getProductMainInfoWithId(int id);
@Insert
void insertProducts(Product... product);


@Update
void updateProducts(Product... product);


@Delete
void deleteProducts(Product... product);


@Query("SELECT * FROM product")
List<Product> getAllProducts();


@Query("SELECT name,price FROM product WHERE id IS :id")
    ProductMainInfo getProductMainInfoWithId(int id);

Synchronous Daos

@Query("SELECT * FROM prduct WHRER id = :id")
LiveData<Product> getProductById(int id);


@Query("SELECT * FROM prduct WHERE id = :id")
Flowable<Product> getProductById(int id);

Asynchronous Daos

background

thread

main

thread

observOn

(Scheduler)

product

product

getProductById(int id);
LiveData<Product>
Flowable<Product>
@Insert
void insertProducts(Product... product);


@Update
void updateProducts(Product... product);


@Delete
void deleteProducts(Product... product);


@Query("SELECT * FROM product")
List<Product> getAllProducts();


@Query("SELECT name,price FROM product WHERE id IS :id")
    ProductMainInfo getProductMainInfoWithId(int id);

Transaction

@Dao
public abstract class ProductDao {

    @Insert
    abstract void insertProducts(Product... product);

    @Delete
    abstract deleteProducts(Product... product);


    @Transaction
    public void replaceProducts(Products[] oldProducts
            , Products[] newProducts){

        deleteProducts(oldProducts);
        insertProducts(newProducts);
    }    

}

@Database

SingleTone

dagger / singleTone pattern

@Database(entities = {Product.class}, version = 1)
public abstract class StoreDatabase extends RoomDatabase {


















}
    public abstract ProductDao productDao();
    private static final String DB_NAME = "store";

    private static volatile StoreDatabase INSTANCE;



    public static StoreDatabase getInstance(Context context) {
        synchronized (StoreDatabase.class) {
            if (INSTANCE == null) {
                INSTANCE = Room.databaseBuilder(context,
                        StoreDatabase.class, DB_NAME)
                        .build();
            }
            return INSTANCE;
        }
    }

Using DataBase

//initialize StoreDatabase
StoreDatabase db = StoreDatabase.getInstance(this);

//insert a product
db.productDao().insertProducts(new Product("water",3000));
        
//retrieve inserted product
List<Product> products  = db.productDao().getAllProducts();

Not in Main Thread 

disposable = db.productDao().getProductById(10)

         .subscribeOn(Schedulers.io())

         .observeOn(AndroidSchedulers.mainThread())

         .subscribe(product ->
                     txtResult.setText(product.getName()),

                 throwable ->
                     Log.e("db", "Unable to get productName", throwable));


//onStop
disposable.dispose();

Flowable

db.productDao().getProductById(10)
        .observe(this, new Observer<Product>() {

            @Override
            public void onChanged(@Nullable Product product) {

                if (product != null) {
                    txtResult.setText(product.getName());
                } else {
                    txtResult.setText("data is not available!");
                }

            }
        });

LiveData

Test Database

Test Daos

@RunWith(AndroidJUnit4.class)
public class ProductDaoTest {

    private StoreDatabase database;

    private ProductDao productDao;



    private Product PRODUCT = new Product(10, "soda");

    @Before
    public void initDb() throws Exception {

        database = Room.inMemoryDatabaseBuilder(
                InstrumentationRegistry.getContext(),
                StoreDatabase.class)
                .build();

        productDao = database.productDao();
    }


    @After
    public void closeDb() throws Exception {
        database.close();
    }

    @Test
    public void productCanBeRetrievedAfterInsert() throws Exception {
        //insert a product
        productDao.insertProducts(PRODUCT);

        //get inserted product by id
        Product actual = productDao
            .getProductWithId(PRODUCT.getId());

        //Test
        assertEquals(PRODUCT.getId(), actual.getId());
        assertEquals(PRODUCT.getName(), actual.getName());
    }
}
@Rule
public InstantTaskExecutorRule instantTaskExecutorRule
     = new InstantTaskExecutorRule();


 @Test
    public void productCanBeRetrievedAfterInsertWithLiveData() 
            throws Exception {

        //insert a product
        productDao.insertProducts(PRODUCT);

        //get inserted product by id using LiveData
        LiveData<Product> actual = productDao
                .getProductWithIdByLiveData(PRODUCT.getId());

        //getData from LiveData
        Product actualProduct = LiveDataTestUtil.getValue(actual);
        //Test
        assertEquals(PRODUCT.getId(), actualProduct.getId());
        assertEquals(PRODUCT.getName(), actualProduct.getName());
    }
@Test
    public void productCanBeRetrievedAfterInsertWithRXJava()
         throws Exception {

        //insert a product
        productDao.insertProducts(PRODUCT);

        //get inserted product by id using Flowable
        productDao
                .getProductWithIdByRxJava(PRODUCT.getId())
                .test().assertValue(actualProduct ->
                actualProduct.getId() == PRODUCT.getId()
                        && actualProduct.getName()
                            .equals(PRODUCT.getName()));
    }

Save that Bitmap

public class BitmapConverter {

    @TypeConverter
    public Bitmap toBitmap(byte[] byteArray) {
        return BitmapFactory
            .decodeByteArray(byteArray, 0, byteArray.length);
    }

    @TypeConverter
    public byte[] toByteArray(Bitmap bitmap) {

        ByteArrayOutputStream stream =
             new ByteArrayOutputStream();

        bitmap.compress(Bitmap.CompressFormat.PNG
            , 100, stream);

        return stream.toByteArray();
    }

}

@Database(entities = {Product.class}, version = 2)
@TypeConverters(BitmapConverter.class)
public abstract class StoreDatabase extends RoomDatabase {

....

}

@Entity(tableName = "product")
public class Product {

    ...

    @ColumnInfo(typeAffinity = ColumnInfo.BLOB)
    private Bitmap picture;

    ...

}

Relations

One To Many 

@Entity(tableName = "user")
public class User {

    @PrimaryKey(autoGenerate = true)
    private int id;

    private String identityNumber;

    private String name;
}

User & Sim Cards

@Entity(tableName = "simcard",
        foreignKeys = @ForeignKey(entity = User.class,
                parentColumns = "id",
                childColumns = "userId",
                onDelete = CASCADE))
public class SimCard {

    @PrimaryKey(autoGenerate = true)
    private int id;

    private String number;

    private String serial;

    private int userId;
}
public interface SimCardDao {

    ....

    @Query("SELECT * FROM SimCard WHERE userId=:userId")
    List<SimCard> getSimCardsOfUser(final int userId);

}

Many To Many

@Entity(tableName = "author")
public class Author {

    @PrimaryKey(autoGenerate = true)
    private int id;

    private String name;

    private String identityId;
    
    ....
}

Authors & Books

@Entity(tableName = "book")
public class Book {

    @PrimaryKey(autoGenerate = true)
    private int id;

    private String name;

    private String ISBN;
    
    ....
}

@Entity(tableName = "author_book_join",
        primaryKeys = {"authorId", "bookId"},
        foreignKeys = {
                @ForeignKey(entity = Author.class,
                        parentColumns = "id",
                        childColumns = "authorId"),
                @ForeignKey(entity = Book.class,
                        parentColumns = "id",
                        childColumns = "bookId")
        })
public class AuthorBookJoin {

    private int authorId;

    private int bookId;

    ....

}
public interface AuthorBookJoinDao {

    @Insert
    void insertAuthorBookJoin(AuthorBookJoin authorBookJoin);

    @Update
    void updateAuthorBookJoin(AuthorBookJoin authorBookJoin);

    @Delete
    void deleteAuthorBookJoin(AuthorBookJoin authorBookJoin);


    @Query("SELECT * FROM author INNER JOIN author_book_join " +
            "ON author.id = author_book_join.authorId " +
            "WHERE author_book_join.bookId =:bookId")
    List<Author> getAuthorsForBook(final int bookId);

    @Query("SELECT * FROM book INNER JOIN author_book_join " +
            "ON book.id = author_book_join.bookId " +
            "WHERE author_book_join.authorId =:authorId")
    List<Book> getBooksForAuthor(final int authorId);

}

//insertAuthor
authorDao.insert(new Author(1,
        "Robert Cecil Martin",
        "135491354"));

//insertbook
bookDao.insert(new Book(1, 
        "Clean Code", 
        "9785459008586"));

//insertJoin
authorBookJoinDao.insert(new AuthorBookJoin(1, 1));
//get list of Authors of a book
List<Author> authors = authorBookJoinDao.getAuthorsForBook(1);


//get list of books of an Author
List<Book> books = authorBookJoinDao.getBooksForAuthor(1);

@Relation

Migrations

@Entity(tableName = "product")
public class Product {

    ...

    @ColumnInfo(typeAffinity = ColumnInfo.BLOB)
    private Bitmap picture;

    ...

}

Schema Changed

JSON Hash Changed

@Database(entities = {Product.class}, version = 2)
@TypeConverters(BitmapConverter.class)
public abstract class StoreDatabase extends RoomDatabase {

    public static StoreDatabase getInstance(Context context) {
        synchronized (StoreDatabase.class) {
            if (INSTANCE == null) {
                INSTANCE = Room.databaseBuilder(context,
                        StoreDatabase.class, DB_NAME)

                        .addMigrations(MIGRATION_1_2)

                        .build();
            }
            return INSTANCE;
        }
    }

    public static final Migration MIGRATION_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(@NonNull SupportSQLiteDatabase database) {
            database.execSQL("ALTER TABLE product ADD COLUMN picture BLOB");
        }
    };

}
.adddMigrations(MIGRATION_1_2,
                MIGRATION_2_3,
                MIGRATION_3_4,
                MIGRATION_4_5,
                MIGRATION_2_5)



 //migration from 1 to 5

 MIGRATION_1_2 -- MIGRATION_2_5

Migration Path

 @Rule
    public MigrationTestHelper mMigrationTestHelper =
            new MigrationTestHelper(InstrumentationRegistry
                        .getInstrumentation(),
                    StoreDatabase.class.getCanonicalName(),
                    new FrameworkSQLiteOpenHelperFactory());

Test Migrations

    @Test
    public void migrationFrom1To2_containsCorrectData()
                 throws IOException {

        // Create the database in version 1
        SupportSQLiteDatabase db = 
                mMigrationTestHelper.createDatabase(TEST_DB_NAME, 1);

        // Insert some data
        insertProduct(PRODUCT.getId(), PRODUCT.getName(), db);
        //Prepare for the next version
        db.close();

        // Re-open the database with version 2 and provide MIGRATION_1_2 and
         mMigrationTestHelper.runMigrationsAndValidate(TEST_DB_NAME, 2, true,
                MIGRATION_1_2);

        // Validate that the data was migrated properly.
        Product dbProduct = getMigratedRoomDatabase()
            .productDao().getProductWithId(PRODUCT.getId());

        assertEquals(dbProduct.getId(), PRODUCT.getId());
        assertEquals(dbProduct.getName(), PRODUCT.getName());
    }

Compare ORMs

API
Size
Performance

Title Text

Refrences

Room persistence library

By Ali Nasrabadi

Room persistence library

Is a part of Architecture Components of Google for data persistence.

  • 1,021