DBFlow Guide - TechGeekD/android_guides GitHub Wiki
One of the issues with existing SQL object relational mapping (ORM) libraries is that they rely on Java reflection to define database models, table schemas, and column relationships. DBFlow is one of the few that relies strictly on annotation processing to generate Java code based on the SQLiteOpenHelper framework that avoids this issue. This approach results in increasing run-time performance while also saving you from having to write a lot boilerplate code normally needed to declare your tables, manage their schema changes, and perform queries.
The section below describes how to setup using DBFlow v3, which is currently in beta but still very much ready for production use. If you are upgrading from an older version of DBFlow, read this migration guide. One of the major changes is the library used to generate Java code from annotation now relies on JavaPoet. The generated database and table classes now use '_' instead of '$' as the separator, which may require small adjustments to your code when upgrading.
The first step is to add the android-apt plugin to your root build.gradle
file:
buildscript {
repositories {
// required for this library, don't use mavenCentral()
jcenter()
}
dependencies {
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
Because DBFlow3 is still not officially released, you need also add https://jitpack.io to your allprojects
-> repositories
dependency list:
allprojects {
repositories {
jcenter()
maven { url "https://jitpack.io" }
}
}
Next, within your app/build.gradle, apply the
android-apt` plugin and add DBFlow to your dependency list. We create a separate variable to store the version number to make it easier to change later:
apply plugin: 'com.neenbedankt.android-apt'
def dbflow_version = "3.0.0-beta2"
dependencies {
apt "com.github.Raizlabs.DBFlow:dbflow-processor:${dbflow_version}"
compile "com.github.Raizlabs.DBFlow:dbflow-core:${dbflow_version}"
compile "com.github.Raizlabs.DBFlow:dbflow:${dbflow_version}"
// sql-cipher database encryption (optional)
compile "com.github.Raizlabs.DBFlow:dbflow-sqlcipher:${dbflow_version}"
}
Note: Make sure to remove any previous references of DBFlow if you are upgrading. The annotation processor has significantly changed for older versions. If you java.lang.NoSuchMethodError: com.raizlabs.android.dbflow.annotation.Table.tableName()Ljava/lang/String;
, then you are likely to still have included the old annotation processor in your Gradle configuration.
Annotate your class with the @Database
decorator to declare your database. It should contain both the name to be used for creating the table, as well as the version number. Note: if you decide to change the schema for any tables you create later, you will need to bump the version number. The version number should always be incremented (and never downgraded) to avoid conflicts with older database versions.
@Database(name = MyDatabase.NAME, version = MyDatabase.VERSION)
public class MyDatabase {
public static final String NAME = "MyDataBase";
public static final int VERSION = 1;
}
The Java objects that need to be declared as models need to extend from BaseModel
. In addition, you should annotate the class with the database name too. Here we show how to create an Organization
and User
table:
@Table(database = MyDatabase.class)
public class Organization extends BaseModel {
@Column
@PrimaryKey
int id;
@Column
String name;
}
We can define a ForeignKey
relation easily. The saveForeignKeyModel
denotes whether to update the foreign key if the entry is also updated. In this case, we disable this functionality:
@Table(database = MyDatabase.class)
public class User extends BaseModel {
@Column
@PrimaryKey
int id;
@Column
String name;
@Column
@ForeignKey(saveForeignKeyModel = false)
Organization organization;
public void setOrganization(Organization organization) {
this.organization = organization;
}
public void setName(String name) {
this.name = name;
}
}
If you are using DBFlow with the Parceler library, make sure to annotate the class with the @Parcel(analyze={}
decorator. Otherwise, the Parceler library will try to serialize the fields that are associated with the BaseModel
class and trigger Error:Parceler: Unable to find read/write generator for type
errors. To avoid this issue, specify to Parceler exactly which class in the inheritance chain should be examined (see this disucssion for more details):
@Table(database = MyDatabase.class)
@Parcel(analyze={User.class}) // add Parceler annotation here
public class User extends BaseModel {
}
Next, we need to instantiate DBFlow
in our main application. If you do not have an Application
object, create one:
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
FlowManager.init(this);
}
}
Modify your AndroidManifest.xml
file to call this Application object:
<application
android:name=".MyApplication"
</application>
Basic creation, read, update, and delete (CRUD) statements are fairly straightfroward to do. DBFlow generates a Table class for each your annotated models (i.e. User_Table, Organization_Table). Each field is defined as a Property
object and ensures type-safety when evaluating it against in a SELECT statement or a raw value.
See this section for more details on the queries that can be performed.
We can simply call .save()
on the annotated class to save the row into the table:
// Create organization
Organization organization = new Organization();
organization.setId(1);
organization.setName("CodePath");
organization.save();
// Create user
User user = new User();
user.setName("John Doe");
user.setOrganization(organization);
user.save();
List<Organization> organizationList = new Select().from(Organization.class).queryList();
List<User> users = new Select().from(User.class).where(Organization_Table.name.is("CodePath")).queryList();
Calling save()
on the object will automatically update if the record has already been saved and there is a primary key that matches.
We can simply call .delete()
on the respective object:
user.delete();
See this guide about how to perform transactions. You can batch save a list of User
objects by using the ProcessModelInfo
class:
ArrayList<User> users = new ArrayList<>();
// fetch users from the network
// save rows
TransactionManager.getInstance().addTransaction(new SaveModelTransaction<>(ProcessModelInfo.withModels(users)));
One of the side benefits of using DBFlow is that you can expose tables easily as Android Content Providers, which enables other apps to query this data.
The first step is to declare these Content Providers in the same place where your database is declared to help centralize all the declarations. We also need to expose a URL for other apps to query, which will be declared as content://com.codepath.myappname.provider
in this example.
@ContentProvider(authority = MyDatabase.AUTHORITY,
database = MyDatabase.class,
baseContentUri = MyDatabase.BASE_CONTENT_URI)
@Database(name = MyDatabase.NAME, version = MyDatabase.VERSION)
public class MyDatabase {
public static final String NAME = "MyDatabase";
public static final int VERSION = 1;
public static final String AUTHORITY = "com.codepath.myappname.provider";
public static final String BASE_CONTENT_URI = "content://";
private static Uri buildUri(String... paths) {
Uri.Builder builder = Uri.parse(AppDatabase.BASE_CONTENT_URI + AppDatabase.AUTHORITY).buildUpon();
for (String path : paths) {
builder.appendPath(path);
}
return builder.build();
}
}
Next, within this same class, we will declare a User
endpoint (i.e. content://com.codepath.myappname.provider/User) that can be declared:
public class MyDatabase {
// ...
// Declare endpoints here
@TableEndpoint(name = UserProviderModel.ENDPOINT)
public static class UserProviderModel {
public static final String ENDPOINT = "User";
@ContentUri(path = IdentityProviderModel.ENDPOINT,
type = ContentUri.ContentType.VND_MULTIPLE + ENDPOINT)
public static final Uri CONTENT_URI = buildUri(ENDPOINT);
}
}
The final step is for the Content Provider to be exposed. If you wish for other apps to be able to view this data, set android:exported
to be true. Otherwise, if you only wish the existing application to query this content provider, set the value to be false. Note that DBFlow3 uses underscore (_
) instead of the dollar sign ($
) as the separator:
<provider
android:authorities="com.codepath.myapp.provider"
android:exported="true|false"
android:name=".provider.TestContentProvider_Provider"/>
Because DBFlow requires on annotation processing, sometimes you may need to click Build
-> New Project
to rebuild the source code generated.
If you are using DBFlow with ProGuard, and see Table is not registered with a Database. Did you forget the @Table annotation?
, make sure to include this line in your ProGuard configuration:
-keep class * extends com.raizlabs.android.dbflow.config.DatabaseHolder { *; }
You can go into your app/build/intermediate/classes/com/raizlabs/android/dbflow/config
and look for the GeneratedDatabaseHolder.class
to understand what code is generated.
You can also download and inspect the local database using ADB:
adb pull /data/data/com.codepath.yourappname/databases/MyDatabase*.db
sqlite3 MyDatabase.db
Type .schema
to see how the table definitions are created:
sqlite> .schema
CREATE TABLE android_metadata (locale TEXT);
CREATE TABLE `Organization`(`id` INTEGER,`name` TEXT, PRIMARY KEY(`id`));
CREATE TABLE `User`(`id` INTEGER,`name` TEXT,`organization_id` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`organization_id`) REFERENCES `Organization`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION);
If you intend to use DBFlow with the Gson library, you may get StackOverflow
errors when trying to use Java objects that extend from BaseModel
. In order to avoid these issues, you need to exclude the ModelAdapter
class, which is a field included with BaseModel:
public class DBFlowExclusionStrategy implements ExclusionStrategy {
// Otherwise, Gson will go through base classes of DBFlow models
// and hang forever.
@Override
public boolean shouldSkipField(FieldAttributes f) {
return f.getDeclaredClass().equals(ModelAdapter.class);
}
@Override
public boolean shouldSkipClass(Class<?> clazz) {
return false;
}
}
You then need to create a custom Gson builder to exclude this class:
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.setExclusionStrategies(new ExclusionStrategy[]{new DBFlowExclusionStrategy()});
See this issue for more information.