Introduction
Enterlib for Android is a framework which helps to decouple the application's components into separated logical layers where the communications between them is through well-defined interfaces or also called code contracts. The framework's components help to write reusable, robust and testable pieces of code that can scale with little impact in the rest of the application. In addition, the library provides utilities for:
- Data binding
- Asynchronous operations
- Data validations and conversion
- JSON serialization
- Consuming RESTful HTTP Services
- SQLite ORM
- Messaging
- Extended Views
- ViewModels
Maven Repository
The library is hosted on a maven repository. Therefore, in order to use the library in your projects, you must include an additional repository to your main gradle configuration file as shown below:
buildscript {
repositories {
jcenter()
maven { url "https://dl.bintray.com/ansel86castro/enterlib" }
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.3'
}
}
allprojects {
repositories {
jcenter()
maven { url "https://dl.bintray.com/ansel86castro/enterlib" }
}
}
Once the maven repository is added, you can reference the library in your modules as shown below:
dependencies {
...... other dependencies
compile 'com.cybtans:enterlib:2.0.0@aar'
.......
}
Using the Code
Dependency Injection
Enterlib provides a dependency injection engine through the DependencyContext
class which implements the IDependencyContext
interface. You can use this class for registering objects like singletons or factories for creating object instances. The caching mechanism is controlled by the LifeType
value specified when the dependency is registered. You can have several IDependencyContext
linked together creating a context tree where the children of a IDependencyContext
object are called scopes. Instances are cached inside the scope from where they were requested, following its LifeType
specification. You should call the IDependencyContext.dispose()
method when you are done with your scope. The dispose operation will release the cache and dispose all the objects which implement the com.enterlib.IClosable
or java.io.Closable
interfaces.
The following example will show the best place for creating the root IDependencyContext
and registering your dependencies:
class MainApp extends Application {
IDependencyContext dependencyContext;
@Override
public void onCreate() {
super.onCreate();
dependencyContext = new DependencyContext();
DependencyContext.setContext(dependencyContext);
dependencyContext.registerFactory(IEntityContext.class, new IDependencyFactory(){
@Override
public Object createInstance
(IServiceProvider serviceProvider, Class<?> requestType) {
EntityMapContext dbContext = new EntityMapContext(MainApp.this, "db.db3");
return dbContext;
}
}, LifeType.Default);
dependencyContext.registerType(SomeService.class, LifeType.Scope);
dependencyContext.registerSingleton(Context.class, this);
dependencyContext.registerTypes(IDomainService.class, MyDomainService.class, LifeType.Scope);
IDependencyContext scope = dependencyContext.createScope();
IDomainService service = scope.getService(IDomainService.class);
scope.registerTypes(IScopedDomainService.class, MyScopedDomainService.class, LifeType.Scope);
scope.dispose();
}
}
The LifeType
enumeration indicates how the dependency injection engine will create the instances. It has the following options:
- Default: The object is created every time it's requested.
- Scope: The object is created only one time per scope request, following requests will return the cached instance on the scope.
- Singleton: The same instance is returned for every request. If the singleton was registered using a Factory, then it will be created only one time.
Data Access with Object Relational Mapping (ORM)
Enterlib provides a powerful Object Relational Mapper (ORM) for SQLite Databases on Android. The mapping is driven using specific Java annotations on the model's class declaration as shown below:
@TableMap(name = "Accounts")
public class Account{
@ColumnMap(key = true)
public int Id;
@ColumnMap
public String Name;
@ColumnMap
@ForeingKey(model = Currency.class)
public int CurrencyId;
@ExpressionColumn(expr = "CurrencyId.Name")
public String CurrencyName;
@ExpressionColumn(expr = "CurrencyId.CreateDate")
public Date CurrencyCreateDate;
@ColumnMap(column = "Id")
@ForeingKey(model = Transaction.class, field = "AccountId")
@ExpressionColumn(expr = "SUM(Transactions.Amount)")
public double Balance;
@ColumnMap(column = "Id", nonMapped = true)
@ForeingKey(model = Transaction.class, field = "AccountId")
@ExpressionColumn(expr = "Transactions.Description")
public String Description;
private Currency currency;
public Currency getCurrency(){
return currency;
}
public void setCurrency(Currency value){
this.currency = value;
}
}
The annotations are used to specify columns mapping, relationships or computed columns. The most important annotations are:
TableMap
: Optional and can be used to identify the table for the mapping ColumnMap
: Required to identify the column-field mapping and some argument can be use to add additional information like whether it's writable, a primary key, the column name or the primary key order. ForeignKey
: Specify foreing key relationships where you must set the target class and optionally the field referenced. ExpressionColumn
: Powerful mechanism by which fields of foreing key relationships can be included in the current class declaration or using computed columns like aggregations. You can use this annotation to define data views the same way you can do using SQL views.
The remaining models for the example are shown below:
@TableMap(name = "Transactions")
public class Transaction{
@ColumnMap(key = true, writable = false)
public int Id;
@ColumnMap
public String Description;
@ColumnMap
public double Amount;
@ColumnMap
@ForeingKey(model = Account.class)
public int AccountId;
@ExpressionColumn(expr = "AccountId.Name")
public String AccountName;
@ExpressionColumn(expr = "AccountId.CurrencyId.Name")
public String CurrencyName;
private Account account;
public Account getAccount(){
return account;
}
public void setAccount(Account value){
this.account = value;
}
}
@TableMap(name = "Currencies")
public class Currency{
@ColumnMap(key = true)
public int Id;
@ColumnMap(column = "Code")
public String Name;
@ColumnMap
public Date CreateDate;
}
public class Category{
@ColumnMap(key = true)
public int Id;
@ColumnMap
public String Name;
@ColumnMap(column = "Id", nonMapped = true)
@ForeingKey(model = AccountCategory.class, field = "CategoryId")
public ArrayList<AccountCategory> Accounts;
}
public class AccountCategory{
@ColumnMap(key = true, order = 0)
@ForeingKey(model = Account.class, field = "Id")
public int AccountId;
@ColumnMap(key = true, order = 1)
@ForeingKey(model = Category.class, field = "Id")
public int CategoryId;
}
Enterlib Android also helps for deploying sqlite databases. If you have the SQLite database on a file stored in the app's asset directory like for example named db.db3
, you can easily deploy it using the EntityMapContext
class as shown below:
EntityMapContext.deploy(this, "db.db3");
Then after the database is deployed, the next step is creating an instance of an IEntityContext
. This object represents the database connection and can be used to retrieve IRepository<T>
instances.
IEntityContext context = new EntityMapContext(MainApp.this, "db.db3");
On the other hand, IRepository<T>
instances are used to query or modified the database.
IRepository<Transaction> map = context.getRepository(Transaction.class);
ArrayList<Transaction> list = map.query().toList();
In the example above, the query will generate the following SQL statement when it's evaluated into a List
.
SELECT t0.Description as "Description"
,t0.AccountId as "AccountId"
,t0.Amount as "Amount"
,t0.Id as "Id"
,t1.Name as "AccountName"
,t2.Code as "CurrencyName"
FROM "Transactions" t0
INNER JOIN "Accounts" t1 on t1.Id = t0.AccountId
INNER JOIN "Currencies" t2 on t2.Id = t1.CurrencyId
The IRepository<T>
also provides methods for creating, updating, deleting entities as shown below:
transaction = new Transaction();
map.create(transaction);
map.update(transaction);
map.delete(transaction);
map.delete("Description = 'Abc'");
map.query().count();
transaction = map.query().first();
Lazy Evaluation
Enterlib Android was designed having performance as a priority in mind, so for that reason, the IEntityCursor<T>
was introduced . The IEntityCursor<T>
is a mechanism for iterating through the query in a more efficient way. Meaning entities are loaded on demand, optimizing memory usage and therefore it's the recommended usage for iterating large result sets from queries.
IRepository<Transaction> map = context.getRepository(Transaction.class);
IEntityCursor<Transaction> cursor = map.query().toCursor();
for (Transaction t: cursor ) {
}
cursor.close();
Another example using cursors where the IEntityCursor<T>
is implicitly closed when there are no more elements to iterate.
IRepository<Transaction> map = context.getRepository(Transaction.class);
for (Transaction t: map.query() ) {
}
The IEntityCursor<T>
has the following interface definition:
public interface IEntityCursor<T> extends IClosable, Iterable<T> {
int getCount();
T getItem(int position);
}
Filters Expressions and Functions
Enterlib's ORM for Android supports the following functions in string
expressions for filtering or as arguments in the ExpressionColumn
annotations:
sum
(expression) avg
(expression) count
(expression) max
(expression) min
(expression) concat
(expression): for string
fields, returns the concatenations of the values ifnull
(exp1
, exp2
): returns exp2
if exp1
is null
contains
(expression) exclude
(expression)
Example:
IRepository<Account> map = context.getRepository(Account.class);
IQuerable<Account> querable = map.query()
.include("Currency")
.where("CurrencyId.Name = 'USD'")
.where("AVG(Transactions.Amount) > 5")
.orderBy("Name desc")
.skip(5)
.take(10);
ArrayList<Transaction> list = querable.toList();
The instruction querable.toList()
compiles the query and generates the following SQL:
SELECT t1.CreateDate as "CurrencyCreateDate"
,t0.Id as "Id"
,total(t2.Amount) as "Balance"
,t0.CurrencyId as "CurrencyId"
,t0.Name as "Name"
,t1.Code as "CurrencyName"
,t1.Id as ".Currency.Id"
,t1.CreateDate as ".Currency.CreateDate"
,t1.Code as ".Currency.Name"
FROM "Accounts" t0
INNER JOIN "Currencies" t1 on t1.Id = t0.CurrencyId
LEFT OUTER JOIN "Transactions" t2 on t2.AccountId = t0.Id
WHERE t1.Code = 'USD'
GROUP BY t0.Id,t1.CreateDate,t0.Name,t1.Id,t0.CurrencyId,t1.Code
HAVING avg(t2.Amount) > 5
ORDER BY t0.Name DESC
LIMIT 10
OFFSET 5
Here is another example using the include
method. The include
method will add the related entities into the result set:
IRepository<Transaction> map = context.getRepository(Transaction.class);
IQuerable<Transaction> query = map.query()
.include("Account.Currency")
.where("Account.Currency.Name = 'USD'");
System.out.println(query.toString());
As a result, it will print the following SQL statement:
SELECT t0.Id as "Id"
,t0.Description as "Description"
,t0.Amount as "Amount"
,t0.AccountId as "AccountId"
,t1.Name as "AccountName"
,t2.Code as "CurrencyName"
,t1.Id as ".Account.Id"
,t1.Name as ".Account.Name"
,t1.CurrencyId as ".Account.CurrencyId"
,t2.Code as ".Account.CurrencyName"
,t2.CreateDate as ".Account.CurrencyCreateDate"
,total(t3.Amount) as ".Account.Balance"
,t2.Id as ".Account.Currency.Id"
,t2.Code as ".Account.Currency.Name"
,t2.CreateDate as ".Account.Currency.CreateDate"
FROM "Transactions" t0
INNER JOIN "Accounts" t1 on t1.Id = t0.AccountId
INNER JOIN "Currencies" t2 on t2.Id = t1.CurrencyId
LEFT OUTER JOIN "Transactions" t3 on t3.AccountId = t1.Id
WHERE t2.Code = 'USD'
GROUP BY t2.Code,t0.Description,t1.Name,t2.Id,t0.Amount,t0.Id,
t1.CurrencyId,t1.Id,t0.AccountId,t2.CreateDate
The expressions Account.Currency.Name
and AccountId.CurrencyId.Name
passed in the where
method are equivalent - they produce the same result. Enterlib follows a set of conventions in order to find the foreign keys for a given navigation property.
On the other hand, for using the include
method, you must declare the navigation property to be injected with the related object. For example, below is defined the navigation property for the AccountId
field in the Transaction
class. By convention, it will look for the options AccountId, Accountid, Account_id
when using include(Account)
in order to find the foreign key for the navigation property Account
.
public class Transaction{
private Account account;
public Account getAccount(){
return account;
}
public void setAccount(Account value){
this.account = value;
}
}
Using Aliases for Fields in Associated Models
The expressions supported by the ORM can include aliases for fields of the related models. An alias is nothing more that a short form for referencing those fields that belong to associated models. For example:
IRepository<Account> map = context.getRepository(Account.class);
IQuerable<Account> querable = map.query()
.where("CurrencyName = 'EUR'");
In the previous example, the field CurrencyName
is an alias for CurrencyId.Name
. You could observe the CurrencyName
field is defined using the ExpressionColumn
annotation as follows:
@TableMap(name = "Accounts")
public class Account{
@ExpressionColumn(expr = "CurrencyId.Name")
public String CurrencyName;
}
The resulting query will be compiled into the following SQL statements:
SELECT t1.CreateDate as "CurrencyCreateDate"
,t0.Id as "Id"
,total(t2.Amount) as "Balance"
,t0.CurrencyId as "CurrencyId"
,t0.Name as "Name"
,t1.Code as "CurrencyName"
FROM "Accounts" t0
INNER JOIN "Currencies" t1 on t1.Id = t0.CurrencyId
LEFT OUTER JOIN "Transactions" t2 on t2.AccountId = t0.Id
WHERE t1.Code = 'EUR'
GROUP BY t0.Id,t1.CreateDate,t0.Name,t0.CurrencyId,t1.Code
Advance Filtering
Enterlib provides extended filtering functions like Contains
or Exclude
, note that they are case insensitive. For example, the following query will retrieve all the Category
objects associated with at least five Account
entities:
IRepository<Category> map = context.getRepository(Category.class);
IQuerable<Category> querable = map.query()
.where("CONTAINS(COUNT(Accounts.AccountId) > 5)");
When the query evaluates, it will produce the following SQL statement:
SELECT t0.Id as "Id"
,t0.Name as "Name"
FROM "Category" t0
WHERE t0.Id IN (SELECT t0.CategoryId as "CategoryId"
FROM "AccountCategory" t0
GROUP BY t0.CategoryId
HAVING count(t0.AccountId) > 5)
The same way using the exclude
function you can query for example all the Category
objects which are not associated to any Account
entity with Currency
'UYU'
.
IRepository<category> map = context.getRepository(Category.class);
IQuerable<category> querable = map.query()
.where("EXCLUDE(Accounts.AccountId.CurrencyId.Name = 'UYU')");
SELECT t0.Id as "Id"
,t0.Name as "Name"
FROM "Category" t0
WHERE t0.Id NOT IN (SELECT t0.CategoryId as "CategoryId"
FROM "AccountCategory" t0
INNER JOIN "Accounts" t1 on t1.Id = t0.AccountId
INNER JOIN "Currencies" t2 on t2.Id = t1.CurrencyId
WHERE t2.Code = 'UYU')
Points of Interest
In this article, we learned how we can leverage the Dependency Injection engine provided by Enterlib in order to create more modular and decoupled architecture for our apps. The Object Relational Mapping Engine shows a good potential for saving time when working with offline data. As shown before, it supports very expressive string
expressions for filtering and mapping configurations.