65.9K
CodeProject 正在变化。 阅读更多。
Home

利用 Enterlib Android 功能

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2018 年 5 月 28 日

CPOL

6分钟阅读

viewsIcon

8893

本文介绍了一些 Enterlib for Android 的激动人心的功能,例如其对象关系映射器和依赖注入引擎。

引言

Enterlib for Android 是一个框架,它有助于将应用程序的组件解耦为分离的逻辑层,这些层之间的通信通过明确定义的接口或称为代码契约进行。该框架的组件有助于编写可重用、健壮且可测试的代码片段,这些代码片段可以随着应用程序的其余部分影响很小而扩展。此外,该库还提供了用于以下方面的实用工具:

  • 数据绑定
  • 异步操作
  • 数据验证和转换
  • JSON 序列化
  • 使用 RESTful HTTP 服务
  • SQLite ORM
  • 消息传递
  • 扩展视图
  • ViewModels

Maven 仓库

该库托管在一个 maven 仓库中。因此,要在您的项目中使用该库,您必须在主 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" }
    }
}

添加 Maven 仓库后,您可以在模块中引用该库,如下所示:

dependencies {
 ...... other dependencies

    compile 'com.cybtans:enterlib:2.0.0@aar'
 
 .......
}

Using the Code

依赖注入

Enterlib 通过 DependencyContext 类提供了一个依赖注入引擎,该类实现了 IDependencyContext 接口。您可以使用此类来注册单例或工厂等对象来创建对象实例。缓存机制由注册依赖项时指定的 LifeType 值控制。您可以将多个 IDependencyContext 对象链接在一起,创建一个上下文树,其中 IDependencyContext 对象的子对象称为作用域。实例在它们被请求的作用域内缓存,遵循其 LifeType 规范。当您完成作用域的使用后,应该调用 IDependencyContext.dispose() 方法。dispose 操作将释放缓存并销毁所有实现 com.enterlib.IClosablejava.io.Closable 接口的对象。

以下示例将展示创建根 IDependencyContext 并注册依赖项的最佳位置:

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

LifeType 枚举指示依赖注入引擎将如何创建实例。它具有以下选项:

  • 默认:每次请求时都会创建对象。
  • 作用域:对象仅在每个作用域请求时创建一次,后续请求将返回作用域中的缓存实例。
  • 单例:每次请求都返回相同的实例。如果单例是使用 Factory 注册的,那么它将只创建一次。

使用对象关系映射 (ORM) 进行数据访问

Enterlib 为 Android 上的 SQLite 数据库提供了一个强大的对象关系映射器 (ORM)。映射是使用模型类声明中的特定 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;  
		}  
	}

这些注解用于指定列映射、关系或计算列。最重要的注解是:

  • TableMap:可选,可用于标识映射的表
  • ColumnMap:必需,用于标识列-字段映射,并且可以使用一些参数添加额外信息,例如是否可写、主键、列名或主键顺序。
  • ForeignKey:指定外键关系,您必须设置目标类,还可以选择设置引用的字段。
  • ExpressionColumn:一种强大的机制,通过它可以将外键关系中的字段包含在当前类声明中,或使用计算列(如聚合)。您可以使用此注解以与使用 SQL 视图相同的方式定义数据视图。

示例中的其余模型如下所示:

   @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 还支持部署 sqlite 数据库。如果您在应用程序资源目录中的文件(例如,命名为 db.db3)中存储了 SQLite 数据库,您可以使用 EntityMapContext 类轻松部署它,如下所示:

 EntityMapContext.deploy(this, "db.db3");

然后,部署数据库后,下一步是创建一个 IEntityContext 实例。此对象代表数据库连接,可用于检索 IRepository<T> 实例。

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

另一方面,IRepository<T> 实例用于查询或修改数据库。

IRepository<Transaction> map = context.getRepository(Transaction.class);
ArrayList<Transaction> list = map.query().toList();

在上面的示例中,当查询被计算为 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 

IRepository<T> 还提供了用于创建、更新、删除实体的​​方法,如下所示:

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

惰性求值

Enterlib Android 在设计时优先考虑性能,因此引入了 IEntityCursor<T>IEntityCursor<T> 是一种更有效地迭代查询的机制。这意味着实体是按需加载的,从而优化了内存使用,因此建议在迭代查询结果集很大时使用它。

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

另一个使用游标的示例,其中 IEntityCursor<T> 在没有更多可迭代元素时被隐式关闭。

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

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

IEntityCursor<T> 具有以下接口定义:

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

过滤器表达式和函数

Enterlib 的 Android ORM 支持 string 表达式中的以下函数,用于过滤或作为 ExpressionColumn 注解中的参数:

  • sum(expression)
  • avg(expression)
  • count(expression)
  • max(expression)
  • min(expression)
  • concat(expression):对于 string 字段,返回值的连接。
  • ifnull(exp1, exp2):如果 exp1null,则返回 exp2
  • contains(expression)
  • exclude(expression)

示例

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

querable.toList() 指令编译查询并生成以下 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

这是另一个使用 include 方法的示例。include 方法会将关联的实体添加到结果集中。

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

因此,它将打印以下 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

where 方法中传递的表达式 Account.Currency.NameAccountId.CurrencyId.Name 是等效的——它们产生相同的结果。Enterlib 遵循一组约定来查找给定导航属性的外键。

另一方面,要使用 include 方法,您必须声明要注入关联对象的导航属性。例如,下面定义了 Transaction 类中 AccountId 字段的导航属性。根据约定,它会在使用 include(Account) 时查找选项 AccountId, Accountid, Account_id,以便为导航属性 Account 查找外键。

     public class Transaction{
        // other code ....
        
        private Account account;

        public Account getAccount(){
            return account;
        }

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

为关联模型中的字段使用别名

ORM 支持的表达式可以包含关联模型的字段的别名。别名只不过是引用属于关联模型的字段的简写形式。例如:

IRepository<Account> map = context.getRepository(Account.class);
IQuerable<Account> querable  = map.query()
                .where("CurrencyName = 'EUR'");

在前面的示例中,字段 CurrencyNameCurrencyId.Name 的别名。您可以看到 CurrencyName 字段是使用 ExpressionColumn 注解定义的,如下所示:

    @TableMap(name = "Accounts")
    public class Account{
        // other code ....
        
        @ExpressionColumn(expr = "CurrencyId.Name")
        public String CurrencyName;
       
       // other code ....
    }

生成的查询将被编译成以下 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

高级过滤

Enterlib 提供了扩展过滤函数,如 ContainsExclude,请注意它们不区分大小写。例如,以下查询将检索至少与五个 Account 实体**关联**的所有 Category 对象:

IRepository<Category> map = context.getRepository(Category.class);
IQuerable<Category> querable  = map.query()
                .where("CONTAINS(COUNT(Accounts.AccountId) > 5)");

当查询求值时,它将生成以下 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)

同样,使用 exclude 函数,您可以查询例如所有**未与**任何 Account 实体**关联**的 Category 对象,该实体具有 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') 

关注点

在本文中,我们学习了如何利用 Enterlib 提供的依赖注入引擎,为我们的应用程序创建更模块化和解耦的架构。对象关系映射引擎在处理离线数据时显示出节省时间的巨大潜力。如前所述,它支持非常丰富的 string 表达式用于过滤和映射配置。

© . All rights reserved.