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