Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / Android

Leveraging Enterlib Android Features

5.00/5 (2 votes)
8 Jun 2018CPOL6 min read 8.3K  
The article describes some exciting features of Enterlib for Android like its Object Relational Mapper and the Dependency Injection Engine.

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:

gradle
buildscript {
    repositories {
        jcenter()
        maven { url "https://dl.bintray.com/ansel86castro/enterlib" }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

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:

Java
class MainApp  extends Application {
 	
    IDependencyContext dependencyContext;

	@Override
 	public void onCreate() {
        super.onCreate();

        dependencyContext = new DependencyContext();
        DependencyContext.setContext(dependencyContext);

        //Register a  factory gives flexibility for creating instances. 
        //Other dependencies can be requested using the IServiceProvider parameter       
        
        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);
         
         // Creating a scope 
         IDependencyContext scope = dependencyContext.createScope();
         
         //Objects resolved from the scope that has LifeType.Scope 
         //when registered in the scope or in any of its parent's scopes are cached 
         //so the same instance is reused per scope request.                        
         IDomainService service = scope.getService(IDomainService.class);
         
         //Registering types for using in a scope only
         scope.registerTypes(IScopedDomainService.class, MyScopedDomainService.class, LifeType.Scope);
         
         //disposing a scope frees from the scope cache all objects with 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:

Java
@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;

     /// Definitions for navigation properties

     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:

Java
@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:

Java
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.

Java
IEntityContext context = new EntityMapContext(MainApp.this, "db.db3");

On the other hand, IRepository<T> instances are used to query or modified the database.

Java
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.

SQL
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:

Java
 //Creates a new entity in the persisting store
 transaction = new Transaction();
 map.create(transaction);

 //update the entity in the persisting store with new values
 map.update(transaction);

//delete the entity from the persisting store
 map.delete(transaction);

 //delete all entities satisfying the condition
 map.delete("Description = 'Abc'");

//returns the total count of entities
 map.query().count();

 //return the first element in the query
 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.

Java
IRepository<Transaction> map = context.getRepository(Transaction.class); 
IEntityCursor<Transaction> cursor = map.query().toCursor();
for (Transaction t: cursor ) {  
      //do something with t
}
cursor.close();

Another example using cursors where the IEntityCursor<T> is implicitly closed when there are no more elements to iterate.

Java
IRepository<Transaction> map = context.getRepository(Transaction.class); 

for (Transaction t: map.query() ) {  
      //do something with t
}

The IEntityCursor<T> has the following interface definition:

Java
public interface IEntityCursor<T> extends IClosable, Iterable<T> {  
		//return the total of elements in the query
	   int getCount();  
	  
	  //return an element if at the specified position
	  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:

Java
 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:

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:

Java
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:

SQL
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.

Java
public class Transaction{
   // other code ....

   private Account account;

   public Account getAccount(){
       return account;
   }

   public void setAccount(Account value){
       this.account = value;
   }

  // other code ....
}

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:

Java
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:

Java
@TableMap(name = "Accounts")
public class Account{
    // other code ....

    @ExpressionColumn(expr = "CurrencyId.Name")
    public String CurrencyName;

   // other code ....
}

The resulting query will be compiled into the following SQL statements:

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"
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:

Java
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:

SQL
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'.

Java
IRepository<category> map = context.getRepository(Category.class);
IQuerable<category> querable  = map.query()
                .where("EXCLUDE(Accounts.AccountId.CurrencyId.Name = 'UYU')");
SQL
 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)