An SQLite ORM for Android with automatic database migrations built with annotation processing
HighLite is an SQLite library for Android that makes use of annotation processing to generate boilerplate for your SQLite operations.
Key features:
Other positives:
dependencies {
compile 'com.jeppeman:highlite:1.1.3'
annotationProcessor 'com.jeppeman:highlite-compiler:1.1.3'
}
Kotlin users will have to replace annotationProcessor
with kapt
.
Annotate a class with @SQLiteDatabaseDescriptor
as follows:
@SQLiteDatabaseDescriptor(
dbName = "companyDatabase",
dbVersion = 1 // Increment this to trigger an upgrade
)
public class CompanyDatabase {
// Optional: define a method like this if you want to manually handle onOpen.
// Note: PRAGMA foreign_keys = ON is set automatically if any foreign
// keys are found for any table in the database.
@OnOpen
public static void onOpen(SQLiteDatabase db) {
...
}
// Optional: define a method like this if you want to manually handle onCreate;
// i.e. if you opt out from automatic table creation on some table.
@OnCreate
public static void onCreate(SQLiteDatabase db) {
...
}
// Optional: define a method like this if you want to manually handle onUpgrade;
// i.e. if you opt out from automatic upgrades on some table
@OnUpgrade
public static void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
...
}
}
Then define a class for a table that links to the database class
@SQLiteTable(
database = CompanyDatabase.class,
tableName = "companies", // If left empty, the name of the table defaults to the class name snake cased
autoCreate = true, // defaults to true, set to false if you do not want the table to be created automatically
autoAddColumns = true, // defaults to true, set to false if you do not want new columns to be added automatically on upgrades
autoDeleteColumns = false // defaults to false, set to true if you want deleted fields to be removed from the database automatically on upgrades
)
public class Company {
@SQLiteColumn(primaryKey = @PrimaryKey(autoIncrement = true))
long id; // fields annotated with @SQLiteColumn need to be at least package local
@SQLiteColumn("companyName") // Column name becomes companyName here
String name;
@SQLiteColumn
Date created; // Dates are stored as INTEGER's with the amount of seconds since UNIX epoch
@SQLiteColumn
List<String> employees; // Fields with types that cannot be matched against an SQLite data type will be serialized and stored as BLOB's
}
That's it, you're now ready to start doing some actual database operations.
Note to Kotlin users
For now, Kotlin properties with primitive types have to be annotated with @JvmField
, and non-primitive have to be marked with lateinit
or be annotated with @JvmField
. Here follows an example:
@SQLiteTable(database = CompanyDatabase::class)
class Company {
@JvmField
@SQLiteColumn(primaryKey = PrimaryKey(autoIncrement = true))
var id : Int = 0 // Primitive type, annotate with @JvmField
@SQLiteColumn("companyName")
lateinit var name : String // Non-primitive, mark with lateinit
}
The reason for this is that a Kotlin property by default is compiled to a private Java field with a getter
and a setter method. With @JvmField
and lateinit
the compiled java class has its corresponding field exposed publicly
SQLiteOperator<Company> operator = SQLiteOperator.from(getContext(), Company.class);
final Company companyObject = new Company();
companyObject.name = "My awesome company";
companyObject.created = new Date();
companyObject.employees = Arrays.asList("John", "Bob");
// Blocking
operator.save(companyObject).executeBlocking(); // the save method inserts if the object's id is not present in the table, otherwise updates
// Non-blocking
operator.save(companyObject)
.asCompletable() // or .asFlowable(), .asObservable(), .asSingle() or .asMaybe();
// If you pass an argument to getSingle it will be matched against the table's primary key field,
// in this case `id` = 1
final Company fetchedObject = operator.getSingle(1).executeBlocking();
fetchedObject.name = "My not so awesome company";
operator.save(fetchedObject).executeBlocking();
final List<Company> list = operator
.getList()
.withQuery(
SQLiteQuery
.builder()
.where("`id` = ? AND `companyName` = ?", 1, "My not so awesome company")
.build()
).executeBlocking();
final List<Company> list = operator
.getList()
.withRawQuery("SELECT * FROM companies where `id` = ?", 1)
.executeBlocking();
operator.delete(list).executeBlocking();
operator
.delete()
.withQuery(
SQLiteQuery
.builder()
.where("`id` = ?", 1)
.build()
).executeBlocking();
operator
.save()
.withQuery(
SQLiteQuery
.builder()
.set("companyName", "Changed name")
.set("created", new Date())
.where("`id` = ?", 1)
.build()
).executeBlocking();
HighLite supports foreign keys and relationships, here's an example of how you can use them:
@SQLiteTable(
database = CompanyDatabase.class,
tableName = "companies"
)
public class Company {
@SQLiteColumn(primaryKey = @PrimaryKey(autoIncrement = true))
long id;
@SQLiteColumn("companyName")
String name;
@SQLiteRelationship(backReference = "company") // backReference needs to be the name of the foreign key field of the class it is referring to
List<Employee> employeeList; // When a company is fetched from the database, its related employees gets fetched as well
}
@SQLiteTable(
database = CompanyDatabase.class,
tableName = "employees"
)
public class Employee {
@SQLiteColumn(primaryKey = @PrimaryKey(autoIncrement = true))
long id; // fields annotated with @SQLiteColumn need to be package local
@SQLiteColumn("employeeName")
String name;
@SQLiteColumn
float salary;
@SQLiteColumn(foreignKey = @ForeignKey(
fieldReference = "id", // Note: this is the name of the field of the class you are referring to, not the database column name; the field has to be unique
cascadeOnDelete = true, // defaults to false
cascadeOnUpdate = true // defaults to false
))
Company company; // When an employee is fetched, this field is automatically instantiated as its corresponding Company
}
Let's create a company with a couple of employees:
SQLiteOperator<Company> companyOperator = SQLiteOperator.from(getContext(), Company.class);
Company company = new Company();
company.name = "My awesome company";
companyOperator.save(company).executeBlocking();
SQLiteOperator<Employee> employeeOperator = SQLiteOperator.from(getContext(), Employee.class);
Employee john = new Employee(),
bob = new Employee();
john.name = "John";
john.salary = 1000f;
john.company = company;
bob.name = "Bob";
bob.salary = 10000f;
bob.company = company;
employeeOperator.save(john, bob).executeBlocking();
Now if we fetch the company from the database the employees will follow:
Company companyFromDatabase = companyOperator
.getSingle()
.withRawQuery("SELECT * FROM companies WHERE `name` = ?", "My awesome company")
.executeBlocking();
Log.d("employees", companyFromDatabase.employeeList /* <- this is now [john, bob]*/)
HighLite supports inheritance of classes annotated with SQLiteTable
, consider the following:
@SQLiteTable(
database = CompanyDatabase.class
)
public class Developer extends Employee {
@SQLiteColumn
String type;
}
Here the class Developer
extends Employee
, which is already annotated with SQLiteTable
, the
create statement that is generated from this setup looks like this:
CREATE TABLE IF NOT EXISTS developer (
`type` TEXT,
`employees_ptr_id` INTEGER PRIMARY KEY NOT NULL,
FOREIGN KEY(`employees_ptr_id`) REFERENCES employees(`id`) ON DELETE CASCADE ON UPDATE CASCADE
);
So we have a one-to-one relationship between Developer
and Employee
, therefore the primary key
for Developer
is automatically created as a pointer to the primary key of Employee
.
Let's have a look at what happens when we use operations on the Developer
class.
SQLiteOperator<Developer> operator = SQLiteOperator.from(getContext(), Developer.class);
Developer dev = new Developer();
dev.name = "Bob";
dev.salary = 10000f;
dev.company = company;
dev.type = "Android";
// When we save the object, the values of the fields are saved to the table they correspond to in
// the class hierarchy; in this case, name, salary and company are saved to the employees table,
// whereas type is saved to the developer table
operator.save(dev).executeBlocking();
// Now if we fetch all developers from the database, a JOIN will be performed on the developer and
// employees tables and fields will be populated accordingly.
List<Developer> devsFromDb = operator.getList().executeBlocking();
You may also want to inherit from a base class that is not corresponding to a table in the database, in that case, the following works:
public class TimestampedModel {
@SQLiteColumn
Date created;
@SQLiteColumn
Date modified;
}
@SQLiteTable(
database = CompanyDatabase.class,
tableName = "companies"
)
public class Company extends TimestampedModel {
@SQLiteColumn(primaryKey = @PrimaryKey(autoIncrement = true))
long id;
@SQLiteColumn("companyName")
String name;
@SQLiteRelationship(backReference = "company") // backReference needs to be the name of the foreign key field of the class it is referring to
List<Employee> employeeList; // When a company is fetched from the database, its related employees gets fetched as well
}
With this setup, the following create statement is generated:
CREATE TABLE IF NOT EXISTS companies (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`companyName` TEXT,
`created` INTEGER,
`employees` BLOB,
`created` INTEGER,
`modified` INTEGER
);