HighLite is an SQLite library for Android that makes use of annotation processing to generate boilerplate for your SQLite operations.
Key features:
- Automated table creation and table upgrades (column additions / changes / deletions are automatic) with opt-out possibilities for those who do not want it.
- Query builder that eliminates the need to have to deal with the null-argument passing to the standard Android SQLite API.
- Easy to use API with simple but flexible operations for get, save and delete.
- Reactive! Each operation can be Rx-ified for those who use RxJava.
- Supports inheritance of database models
- Support for foreign keys and relationships in database models.
- Support for the rest of the column constraints available for SQLite, i.e. UNIQUE, NOT NULL and AUTOINCREMENT
- Support for multiple databases
Other positives:
- Fast! No reflection resolving at runtime, all operations are carried out through compile time generated code
- Errors in user setup are caught and reported at compile time
- Lint warnings for errors that can't be caught at compile time
- Comprehensive test coverage
- Type safe operations
- No need to subclass SQLiteOpenHelper; all necessary interactions with it are done under the hood.
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
);
- More flexibility when it comes to the migrations
- Composite primary key support
- Kotlin improvements