Skip to content

An SQLite ORM for Android with automatic database migrations built with annotation processing

License

Notifications You must be signed in to change notification settings

jeppeman/HighLite

Repository files navigation

Build Status

HighLite

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.

Getting started

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.

Basic setup

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

Operations

Insert an object

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();

Fetch by id and update

// 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();

Fetch by query

final List<Company> list = operator
    .getList()
    .withQuery(
        SQLiteQuery
            .builder()
            .where("`id` = ? AND `companyName` = ?", 1, "My not so awesome company")
            .build()
    ).executeBlocking();

Fetch by raw query and delete

final List<Company> list = operator
    .getList()
    .withRawQuery("SELECT * FROM companies where `id` = ?", 1)
    .executeBlocking();

operator.delete(list).executeBlocking();

Delete by query

operator
    .delete()
    .withQuery(
        SQLiteQuery
            .builder()
            .where("`id` = ?", 1)
            .build()
    ).executeBlocking();

Save by query

operator
    .save()
    .withQuery(
        SQLiteQuery
            .builder()
            .set("companyName", "Changed name")
            .set("created", new Date())
            .where("`id` = ?", 1)
            .build()
    ).executeBlocking();

Foreign keys and relationships

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]*/)

Inheritance

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
);

Upcoming features

  • More flexibility when it comes to the migrations
  • Composite primary key support
  • Kotlin improvements

About

An SQLite ORM for Android with automatic database migrations built with annotation processing

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages