Android: data persistence Romain Rochegude 2016.09.30 1
Introduction 2
Introduction • POO basics: modeling the domain • Modeling domain objects and their interactions • Data bound with a remote API • Need of a local database 3
The native way 4
The “raw” way private static final String SQL_CREATE_ENTRIES = "CREATE TABLE REPO (" + "_ID INTEGER PRIMARY KEY," + "NAME TEXT)"; private static final String SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS REPO "; 5
• Subclass SQLiteOpenHelper public class ReposDbHelper extends SQLiteOpenHelper { public static final int DATABASE_VERSION = 1; public static final String DATABASE_NAME = "repos.db"; public ReposDbHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } 6
//... public void onCreate(SQLiteDatabase db) { db.execSQL(SQL_CREATE_ENTRIES); } public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL(SQL_DELETE_ENTRIES); onCreate(db); } } 7
• Get an instance of SQLiteOpenHelper ReposDbHelper dbHelper = new ReposDbHelper(getContext()); 8
• Put Information into a Database SQLiteDatabase db = dbHelper.getWritableDatabase(); ContentValues values = new ContentValues(); values.put("name", "a sample name"); long newRowId = db.insert("REPO", null, values); 9
• Read Information from a Database SQLiteDatabase db = dbHelper.getReadableDatabase(); String[] projection = { "_id", "name" }; String selection = "NAME = ?"; String[] selectionArgs = { "a sample name" }; String sortOrder = "NAME DESC"; 10
Cursor cursor = db.query( "REPO", projection, selection, selectionArgs, null, null, sortOrder); cursor.moveToFirst(); long itemId = cursor.getLong( cursor.getColumnIndexOrThrow("_ID") ); 11
The ContentProvider way • Provide a ContentProvider subclass dedicated to the application public class RepoProvider extends ContentProvider { } 12
• Define a specific UriMatcher and configure available URI public class RepoProvider extends ContentProvider { private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); } 13
static { sUriMatcher.addURI("fr.test.app.provider", "repo", 1); sUriMatcher.addURI("fr.test.app.provider", "repo/#", 2); } 14
• Override various CRUD methods public Cursor query( Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { //... 15
//... switch (sUriMatcher.match(uri)) { case 2: selection = selection + "_ID = " + uri.getLastPathSegment(); break; default: //... } 16
• Use it through a ContentResolver instance mCursor = getContentResolver().query( "fr.test.app.provider/repo/2", mProjection, mSelectionClause, mSelectionArgs, mSortOrder); 17
• Refer to the open-sourced Google’s iosched application • Helpful libraries to simplify the deal with a ContentProvider: • ProviGen • AutoValue: Cursor Extension 18
Async management • Perform CRUD operations outside of the main thread 19
Query data using Loader • To query the ContentProvider in an Activity, make it implementing LoaderManager.LoaderCallbacks<Cursor> public class ReposListActivity extends FragmentActivity implements LoaderManager.LoaderCallbacks<Cursor> { } 20
• Start loading data with a loader identifier getLoaderManager() .initLoader(LOADER_REPOS, null, this); • Implement LoaderCallbacks. . . 21
• . . . to create the CursorLoader @Override public Loader<Cursor> onCreateLoader( int id, Bundle bundle) { if(id == LOADER_REPOS) { return new CursorLoader( this, "fr.test.app.provider/repo", mProjection, null, null, null); } //... } 22
• . . . and deal with result @Override public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { if(loader.getId() == LOADER_REPOS){ // ... } } 23
• See also: AsyncQueryHandler 24
The ORM way 25
The well-known: ORMLite • Declare your model using ORMLite annotations @DatabaseTable(tableName = "REPO") public class RepoEntity { @DatabaseField(columnName = "_ID", generatedId = true) public long _id; @DatabaseField public String name; } 26
• declare the corresponding DAO public class DAORepo extends BaseDaoImpl<RepoEntity, Long> { public DAORepo(ConnectionSource cs) throws SQLException { this(cs, RepoEntity.class); } //... } 27
• subclass OrmLiteSqliteOpenHelper public class DatabaseHelperTest extends OrmLiteSqliteOpenHelper { private static final String DATABASE_NAME = "test.db"; private static final int DATABASE_VERSION = 1; //... 28
//... public DatabaseHelperTest( Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION, R.raw.ormlite_config); } //... 29
//... @Override public void onCreate(SQLiteDatabase db, ConnectionSource cs) { TableUtils.createTable(cs, RepoEntity.class); } @Override public void onUpgrade(SQLiteDatabase db, ConnectionSource cs, int oldVersion, int newVersion) { //... } 30
• get the requested DAO DatabaseHelperTest helper = //... ConnectionSource cs = helper.getConnectionSource(); DatabaseTableConfig<RepoEntity> tableConfig = DatabaseTableConfigUtil.fromClass(cs, RepoEntity.class); DAORepo dao = new DAORepo(cs, tableConfig); 31
• perform CRUD operations // create RepoEntity repo = new RepoEntity("a sample name"); dao.create(repo); 32
// read List<RepoEntity> repos = dao.queryBuilder() .where() .eq("name", "a sample name") .query(); 33
• Performance: orm-gap gradle plugin buildscript { repositories { mavenCentral() } dependencies { classpath 'com.github.stephanenicolas.ormgap' + ':ormgap-plugin:1.0.0-SNAPSHOT' } } apply plugin: 'ormgap' 34
• generate an ORMLite configuration file that boosts DAOs creations • to use this file public RepoDatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION, R.raw.ormlite_config); } 35
The attractive way: requery • Object mapping • SQL generator • RxJava and Java 8 support • No reflection, compile-time processing and generation • Relationships support • Callback method (@PostLoad) • Custom type converters 36
• Define object mapping @Entity abstract class Repo { @Key @Generated int id; String name; } 37
• Easy to perform SQL queries Result<Repo> repos = data .select(Repo.class) .where(Repo.NAME.lower().like("%sample%")) .orderBy(Repo.ID.desc()) .get(); 38
Async management: RxJava • Get a specific instance of SingleEntityStore DatabaseSource dbSource = new DatabaseSource(context, Models.DEFAULT, "test.db", 1); Configuration conf = dbSource.getConfiguration(); SingleEntityStore<Persistable> data = RxSupport.toReactiveStore(new EntityDataStore<>(conf)); 39
• and use it the RX way data.select(RepoEntity.class) .get() .subscribeOn(Schedulers.newThread()) .subscribe(/*...*/) 40
Conclusion 41
Conclusion • Personal assessments of each way ContentProvider ORMLite requery setup - + + performance + - + readability - + + maintainability - + + 42

Android Data Persistence

  • 1.
    Android: data persistence RomainRochegude 2016.09.30 1
  • 2.
  • 3.
    Introduction • POO basics:modeling the domain • Modeling domain objects and their interactions • Data bound with a remote API • Need of a local database 3
  • 4.
  • 5.
    The “raw” way privatestatic final String SQL_CREATE_ENTRIES = "CREATE TABLE REPO (" + "_ID INTEGER PRIMARY KEY," + "NAME TEXT)"; private static final String SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS REPO "; 5
  • 6.
    • Subclass SQLiteOpenHelper publicclass ReposDbHelper extends SQLiteOpenHelper { public static final int DATABASE_VERSION = 1; public static final String DATABASE_NAME = "repos.db"; public ReposDbHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } 6
  • 7.
    //... public void onCreate(SQLiteDatabase db){ db.execSQL(SQL_CREATE_ENTRIES); } public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL(SQL_DELETE_ENTRIES); onCreate(db); } } 7
  • 8.
    • Get aninstance of SQLiteOpenHelper ReposDbHelper dbHelper = new ReposDbHelper(getContext()); 8
  • 9.
    • Put Informationinto a Database SQLiteDatabase db = dbHelper.getWritableDatabase(); ContentValues values = new ContentValues(); values.put("name", "a sample name"); long newRowId = db.insert("REPO", null, values); 9
  • 10.
    • Read Informationfrom a Database SQLiteDatabase db = dbHelper.getReadableDatabase(); String[] projection = { "_id", "name" }; String selection = "NAME = ?"; String[] selectionArgs = { "a sample name" }; String sortOrder = "NAME DESC"; 10
  • 11.
    Cursor cursor =db.query( "REPO", projection, selection, selectionArgs, null, null, sortOrder); cursor.moveToFirst(); long itemId = cursor.getLong( cursor.getColumnIndexOrThrow("_ID") ); 11
  • 12.
    The ContentProvider way •Provide a ContentProvider subclass dedicated to the application public class RepoProvider extends ContentProvider { } 12
  • 13.
    • Define aspecific UriMatcher and configure available URI public class RepoProvider extends ContentProvider { private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); } 13
  • 14.
  • 15.
    • Override variousCRUD methods public Cursor query( Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { //... 15
  • 16.
    //... switch (sUriMatcher.match(uri)) { case2: selection = selection + "_ID = " + uri.getLastPathSegment(); break; default: //... } 16
  • 17.
    • Use itthrough a ContentResolver instance mCursor = getContentResolver().query( "fr.test.app.provider/repo/2", mProjection, mSelectionClause, mSelectionArgs, mSortOrder); 17
  • 18.
    • Refer tothe open-sourced Google’s iosched application • Helpful libraries to simplify the deal with a ContentProvider: • ProviGen • AutoValue: Cursor Extension 18
  • 19.
    Async management • PerformCRUD operations outside of the main thread 19
  • 20.
    Query data usingLoader • To query the ContentProvider in an Activity, make it implementing LoaderManager.LoaderCallbacks<Cursor> public class ReposListActivity extends FragmentActivity implements LoaderManager.LoaderCallbacks<Cursor> { } 20
  • 21.
    • Start loadingdata with a loader identifier getLoaderManager() .initLoader(LOADER_REPOS, null, this); • Implement LoaderCallbacks. . . 21
  • 22.
    • . .. to create the CursorLoader @Override public Loader<Cursor> onCreateLoader( int id, Bundle bundle) { if(id == LOADER_REPOS) { return new CursorLoader( this, "fr.test.app.provider/repo", mProjection, null, null, null); } //... } 22
  • 23.
    • . .. and deal with result @Override public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { if(loader.getId() == LOADER_REPOS){ // ... } } 23
  • 24.
    • See also:AsyncQueryHandler 24
  • 25.
  • 26.
    The well-known: ORMLite •Declare your model using ORMLite annotations @DatabaseTable(tableName = "REPO") public class RepoEntity { @DatabaseField(columnName = "_ID", generatedId = true) public long _id; @DatabaseField public String name; } 26
  • 27.
    • declare thecorresponding DAO public class DAORepo extends BaseDaoImpl<RepoEntity, Long> { public DAORepo(ConnectionSource cs) throws SQLException { this(cs, RepoEntity.class); } //... } 27
  • 28.
    • subclass OrmLiteSqliteOpenHelper publicclass DatabaseHelperTest extends OrmLiteSqliteOpenHelper { private static final String DATABASE_NAME = "test.db"; private static final int DATABASE_VERSION = 1; //... 28
  • 29.
    //... public DatabaseHelperTest( Context context){ super(context, DATABASE_NAME, null, DATABASE_VERSION, R.raw.ormlite_config); } //... 29
  • 30.
    //... @Override public void onCreate(SQLiteDatabasedb, ConnectionSource cs) { TableUtils.createTable(cs, RepoEntity.class); } @Override public void onUpgrade(SQLiteDatabase db, ConnectionSource cs, int oldVersion, int newVersion) { //... } 30
  • 31.
    • get therequested DAO DatabaseHelperTest helper = //... ConnectionSource cs = helper.getConnectionSource(); DatabaseTableConfig<RepoEntity> tableConfig = DatabaseTableConfigUtil.fromClass(cs, RepoEntity.class); DAORepo dao = new DAORepo(cs, tableConfig); 31
  • 32.
    • perform CRUDoperations // create RepoEntity repo = new RepoEntity("a sample name"); dao.create(repo); 32
  • 33.
    // read List<RepoEntity> repos= dao.queryBuilder() .where() .eq("name", "a sample name") .query(); 33
  • 34.
    • Performance: orm-gapgradle plugin buildscript { repositories { mavenCentral() } dependencies { classpath 'com.github.stephanenicolas.ormgap' + ':ormgap-plugin:1.0.0-SNAPSHOT' } } apply plugin: 'ormgap' 34
  • 35.
    • generate anORMLite configuration file that boosts DAOs creations • to use this file public RepoDatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION, R.raw.ormlite_config); } 35
  • 36.
    The attractive way:requery • Object mapping • SQL generator • RxJava and Java 8 support • No reflection, compile-time processing and generation • Relationships support • Callback method (@PostLoad) • Custom type converters 36
  • 37.
    • Define objectmapping @Entity abstract class Repo { @Key @Generated int id; String name; } 37
  • 38.
    • Easy toperform SQL queries Result<Repo> repos = data .select(Repo.class) .where(Repo.NAME.lower().like("%sample%")) .orderBy(Repo.ID.desc()) .get(); 38
  • 39.
    Async management: RxJava •Get a specific instance of SingleEntityStore DatabaseSource dbSource = new DatabaseSource(context, Models.DEFAULT, "test.db", 1); Configuration conf = dbSource.getConfiguration(); SingleEntityStore<Persistable> data = RxSupport.toReactiveStore(new EntityDataStore<>(conf)); 39
  • 40.
    • and useit the RX way data.select(RepoEntity.class) .get() .subscribeOn(Schedulers.newThread()) .subscribe(/*...*/) 40
  • 41.
  • 42.
    Conclusion • Personal assessmentsof each way ContentProvider ORMLite requery setup - + + performance + - + readability - + + maintainability - + + 42