根据上一篇文章【【Ehcache技术专题】「入门到精通」带你一起从零基础进行分析和开发Ehcache框架的实战指南(缓存查询-配置篇)】,让我们知道了如何进行配置Ehcache服务的缓存查询配置以及索引的建立。承接上文,我们会进行相关的实际案例,接下来我们将在该Cache初始化之后注册一个DynamicAttributesExtractor,用于索引元素被查询到的次数hitCount。
我们在userCache初始化后给其注册了一个DynamicAttributesExtractor,在DynamicAttributesExtractor实现类中我们实现了attributesFor方法,在该方法体内我们构造了一个Map,并往其中放入了一个key为hitCount的元素。
@Test
public void dynamicExtractor() {
CacheManager cacheManager = CacheManager.create();
Cache userCache = cacheManager.getCache("userCache");
userCache.registerDynamicAttributesExtractor(new DynamicAttributesExtractor() {
@Override
public Map<String, Object> attributesFor(Element element) {
Map<String, Object> attrMap = new HashMap<String, Object>();
attrMap.put("hitCount", element.getHitCount());
return attrMap;
}
});
this.listSearchableAttrs(userCache); //key、value和name
userCache.put(new Element("1", new User()));
this.listSearchableAttrs(userCache); //key、value、name和hitCount
}
/**
* 输出当前Ehcache中可查询的属性
* @param cache
*/
private void listSearchableAttrs(Ehcache cache) {
Set<Attribute> attrSet = cache.getSearchAttributes();
for (Attribute attr : attrSet) {
System.out.println(attr.getAttributeName());
}
}
当我们往userCache中put或者replace元素的时候,就会触发我们注册的DynamicAttributesExtractor的attributesFor方法,然后Ehcache会对返回的动态可查询属性hitCount进行索引。在下面的代码中,我们的在给userCache注册了DynamicAttributesExtractor之后,马上列出其中包含的可查询属性,这个时候肯定只会包含预定义好的key、value和name,因为我们注册的DynamicAttributesExtractor还没有被执行。之后往其中放入元素之后,userCache中包含的可查询属性才会有通过DynamicAttributesExtractor返回的hitCount。
一个Cache只能注册有一个DynamicAttributesExtractor,当同时注册多个时,后者会将前者覆盖。但是DynamicAttributesExtractor和其它AttributeExtractor是可以并存的,所以因为其它AttributeExtractor是在Cache初始化前定义的,所以DynamicAttributesExtractor不能返回已经通过AttributeExtractor提取过的同名属性。
通过前面的内容我们知道设置可查询属性时除了DynamicAttributesExtractor可以在Cache初始化后再添加可查询属性外,我们的可查询属性必须是在Cache初始化之前进行指定,否则在对Cache进行查询时我们就不能使用该查询属性进行查询。如下面这一段代码,我们在Cache初始化后通过获取其配置信息,再往其对应的Searchalbe对象中新增一个名叫hello的查询属性,那么我们在今后对该Cache进行查询时将不能使用hello属性进行查询。
@Test
public void setSearchAttrInProgram() {
CacheManager cacheManager = CacheManager.create();
Cache cache = cacheManager.getCache("searchableCache");
CacheConfiguration cacheConfig = cache.getCacheConfiguration();
Searchable searchable = cacheConfig.getSearchable();
SearchAttribute searchAttribute = new SearchAttribute();
searchAttribute.name("hello");
searchable.addSearchAttribute(searchAttribute);
this.listSearchableAttrs(cache);
}
由于定义非动态查询属性时需要在Cache初始化时定义,所以当我们需要在程序中定义查询属性时对应的Cache也需要是在程序中声明的才行。下面是在程序中指定可查询属性的一个示例。
@Test
public void setSearchAttrInProgram() {
CacheManager cacheManager = CacheManager.create();
CacheConfiguration cacheConfig = new CacheConfiguration();
cacheConfig.name("cacheName").maxBytesLocalHeap(100, MemoryUnit.MEGABYTES);
//新建一个Searchable对象
Searchable searchable = new Searchable();
//给Cache配置Searchable对象,表明该Cache是一个可查询的Cache
cacheConfig.searchable(searchable);
//新建一个查询属性
SearchAttribute searchAttribute = new SearchAttribute();
//指定查询属性的名称和属性提取器的类名
searchAttribute.name("查询属性名称");
//searchAttribute.className("属性提取器的类名");
//Searchalbe对象添加查询属性
searchable.addSearchAttribute(searchAttribute);
//使用CacheConfig创建Cache对象
Cache cache = new Cache(cacheConfig);
//把Cache对象纳入CacheManager的管理中
cacheManager.addCache(cache);
this.listSearchableAttrs(cache);
}
在Ehcache中是通过一个net.sf.ehcache.search.Query对象来表示一个查询的,通过该对象我们可以对缓存中的元素进行查询,查询条件就是我们之前定义好的可查询属性,而查询结果可以是缓存的key、value或可查询属性,也可以是针对于可查询属性的一些统计结果。
在对Cache进行查询前我们需要先创建一个Query对象。Query对象是通过EhCache接口定义的createQuery()方法创建的,Cache类对它进行了实现。有了Query对象之后,我们需要使用Query对象的addCriteria(Criteria criteria)方法给该Query对象添加一些限制条件来对其中缓存的元素进行筛选,否则返回的结果将是针对于所有的缓存元素的。
@Test
public void search () {
CacheManager cacheManager = CacheManager.create();
Cache userCache = cacheManager.getCache("userCache");
User user;
for (int i=0; i<10; i++) {
user = new User(i, "name"+(i%2), 30+i);
userCache.put(new Element(user.getId(), user));
}
Query query = userCache.createQuery();
}
Criteria是一个接口,在net.sf.ehcache.search.expression定义了其一系列的实现类,我们也可以直接通过new一个Criteria实现类的实例来对Query结果进行筛选。但通常我们不需要这样做,因为Ehcache中已经为我们实现了的Criteria通常已经可以满足我们的需求了。Ehcache中代表查询属性的Attribute类已经为我们提供了获取针对于该属性的各种Criteria的方法。好,现在我们已经知道了可以通过查询属性直接获取到针对于该属性的限制Criteria对象,那么我们该如何获取查询属性呢?
获取查询属性Attribute主要有两种方式,
常用的还是通过getSearchAttribute(String attrName)方法来获取对应的查询属性Attribute。
当调用可查询Cache的getSearchAttribute(String attrName)方法来获取当前缓存的可查询属性时,如果对应名称的可查询属性不存在,则会抛出异常。
CacheManager cacheManager = CacheManager.create();
Cache cache = cacheManager.getCache("userCache");
Attribute<String> name = cache.getSearchAttribute("name");
Attribute类使用了泛型定义,其表示当前属性值的类型。
有了可查询属性Attribute之后,我们就可以通过Attribute类定义的一系列方法获取到当前Attribute的某种限制,从而对Query的查询结果进行筛选。如我们要筛选name为“name1”的查询结果时我们可以通过name.eq(“name1”)来进行筛选。
public void search2() {
CacheManager cacheManager = CacheManager.create();
Cache userCache = cacheManager.getCache("userCache");
User user;
for (int i=0; i<10; i++) {
user = new User(i, "name"+(i%2), 30+i);
userCache.put(new Element(user.getId(), user));
}
//获取名称为name的可查询属性Attribute对象
Attribute<String> name = userCache.getSearchAttribute("name");
//创建一个用于查询的Query对象
Query query = userCache.createQuery();
//给当前query添加一个筛选条件——可查询属性name的值等于“name1”
query.addCriteria(name.eq("name1"));
}
接下来我们来看一下Attribute类为我们提供的获取对应Criteria的方法有哪些。
Attribute方法 | 对应Criteria实现类 | 描述 |
---|---|---|
between | Between | 属性值在给定的范围之间 |
in | InCollection | 在给定的集合之中 |
ne | NotEqualTo | 不等于给定的值 |
eq | EqualTo | 等于给定的值 |
lt | LessThan | 小于给定的值 |
le | LessThanOrEqual | 小于或等于给定的值 |
gt | GreaterThan | 大于给定的值 |
ge | GreaterThanOrEqual | 大于或等于给定的值 |
ilike | ILike | 匹配给定的表达式,表达式中可以使用“*”来代表任意多个字符,使用“?”来代表任意一个字符 |
notIlike | NotILike | 不匹配给定的表达式 |
isNull | IsNull | 等于null |
notNull | NotNull | 不等于null |
那当我们要实现与或非的逻辑时怎么办呢?Criteria为我们提供了对应的方法,分别对应and(Criteria criteria)方法、or(Criteria criteria)方法和not()方法,然后这三个方法的返回结果还是一个Criteria,它们对应的Criteria实现类分别为And、Or和Not。当我们使用Query的addCriteria(Criteria criteria)方法来添加一个筛选条件时默认都是对应的and操作。
下面我们来看一些使用Criteria的例子。先假设我们有如下定义的一个Cache,其中存放的元素的value都是一个User对象,下面将给出一些针对于该Cache使用Criteria进行筛选查询的一些示例。
<cache name="userCache" maxBytesLocalHeap="50M">
<searchable>
<searchAttribute name="name" expression="value.getName()"/>
<searchAttribute name="age"/>
<searchAttribute name="unitNo" expression="value.unit.unitNo"/>
<searchAttribute name="unitName" expression="value.unit.getUnitName()"/>
<searchAttribute name="mobile" expression="value.getMobile()"/>
<searchAttribute name="hitCount" expression="element.getHitCount()"/>
searchable>
cache>
1、年龄在25岁到35岁之间且属于单位002的。
Attribute<Integer> age = userCache.getSearchAttribute("age");
Attribute<String> unitNo = userCache.getSearchAttribute("unitNo");
query.addCriteria(age.between(25, 35).and(unitNo.eq("002")));
//或者使用两次addCriteria
// query.addCriteria(age.between(25, 35)).addCriteria(unitNo.eq("002"));
2、属于单位002或者单位003,手机号码以137开始且年龄大于35岁的。
Attribute<Integer> age = userCache.getSearchAttribute("age");
Attribute<String> unitNo = userCache.getSearchAttribute("unitNo");
Attribute<String> mobile = userCache.getSearchAttribute("mobile");
query.addCriteria(age.gt(35).and(unitNo.eq("002").or(unitNo.eq("003"))).and(mobile.ilike("137*")));
3、不属于单位002且年龄小于30的。
Attribute<Integer> age = userCache.getSearchAttribute("age");
Attribute<String> unitNo = userCache.getSearchAttribute("unitNo");
query.addCriteria(unitNo.ne("002").and(age.lt(30)));
//或者使用not()方法
query.addCriteria(unitNo.eq("002").not().and(age.lt(30)));
一个Query在查询之前,我们必须告诉它需要查询什么内容,也就是说查询的结果中会包含哪些信息。如果在执行查询操作之前没有告诉Query我们要查询什么内容,Ehcache将抛出异常。可以查询的内容包括缓存中存入元素的key、value,可查询属性对应的值,以及针对于当前查询结果中某个可查询属性的统计信息。针对于这四种可以查询内容Query中提供了四个include方法来表示当前Query的查询结果中会包含对应的内容。下面用一个表格来做个展示。
Query方法 | 描述 |
---|---|
includeKeys() | 查询结果中包含所存元素的key |
includeValues() | 查询结果中包含所存元素的value |
includeAttribute(Attribute>… attributes) | 查询结果中要包含的可查询属性 |
includeAggregator(Aggregator… aggregators) | 查询结果中所要包含的统计信息,关于Aggregator将在后文介绍统计的时候进行讲解 |
如下的代码表示我们的查询结果中会包含元素的key、可查询属性name和age对应的值。
Attribute<String> name = userCache.getSearchAttribute("name");
Attribute<Integer> age = userCache.getSearchAttribute("age");
query.includeAttribute(name, age);
在实际应用中,为了让我们的程序具有更好的性能,我们的查询结果最好只包含我们需要的信息。如只需要获取某个属性的值就不必返回整个value。
有了Query之后我们就可以来执行对应的查询操作,获取返回的查询结果。通过调用Query的execute()方法就可以对当前Query执行查询操作,并获取其返回的结果。Ehcache中使用一个Results接口来代表一个Query的查询结果,使用Result接口来代表对应的一条记录。
Results中定义了一个方法all()用于返回查询出来的所有Result组成的List,查询的缓存中有多少元素满足查询条件,查询结果Results中就会包含多少个Result对象。
Result中定义有getKey()、getValue()、getAttribute()和getAggregatorResults()方法用于获取查询结果中对应元素的key、value、可查询属性对应的值,以及针对于当前查询的统计信息组成的List。
Results和Result这两个接口Ehcache中都已经存在对应的实现了,我们在使用时只要直接利用接口来进行操作就可以了。
如果查询结果中不包含对应的信息,那么在Result调用对应方法获取信息时将抛出异常。Results针对于查询结果中是否包含这四方面的信息给我们提供了四个has方法:hasKeys()、hasValues()、hasAttributes()和hasAggregators()。
//执行查询操作,返回查询结果Results
Results results = query.execute();
//获取Results中包含的所有的Result对象
List<Result> resultList = results.all();
if (resultList != null && !resultList.isEmpty()) {
for (Result result : resultList) {
//结果中包含key时可以获取key
if (results.hasKeys()) {
result.getKey();
}
//结果中包含value时可以获取value
if (results.hasValues()) {
result.getValue();
}
//结果中包含属性时可以获取某个属性的值
if (results.hasAttributes()) {
Attribute<String> attribute = userCache.getSearchAttribute("name");
result.getAttribute(attribute);
}
//结果中包含统计信息时可以获取统计信息组成的List
if (results.hasAggregators()) {
result.getAggregatorResults();
}
}
}
当然,如果你已经清楚的知道了查询结果中已经包含了key时你在获取key前就可以不用调用Results的hasKeys()方法进行判断了,其它结果也一样。
Results中的all()方法可以返回当前查询的结果中的所有Result组成的List。另外,Results中还提供了一个range(int start, int count)方法用于获取当前结果集的一个子集,其底层默认实现使用的是List的subList()方法。该方法可以用于对查询结果的分页操作。
默认情况下,我们在对Cache进行查询时,查询结果将返回所有满足查询条件的记录。当返回的记录非常多时,系统可能会因为内存不足而报错。Query中定义了一个maxResults(int maxResults)方法用于限制当前查询返回查询结果的最大记录数。
注意:由于元素过期的问题,我们查询结果中的元素不一定还存在。当我们利用完Results之后,我们需要通过调用Results的discard()方法来释放资源。
Ehcache为我们提供了一个Aggregator接口用于在查询过程中对某个查询属性进行统计。我们可以实现自己的Aggregator,也可以使用Ehcache为我们提供的实现类。Ehcache中已经为我们提供了五个Aggregator实现类,分别是Min、Max、Sum、Count和Average。看了名称我应该就知道这五个Aggregator分别是做什么用的。
Min是求最小值、Max是求最大值、Sum是求和、Count是计数、Average是求平均值。那么在使用这五个Aggregator时也是非常方便的,因为我们的Attribute已经为我们针对这五个Aggregator定义了对应的方法。方法名称就是对应Aggregator实现类简称的首字母小写,如Min在Attribute中就对应min()方法。
当我们需要对某个查询属性进行统计时,我们需要把对应的Aggregator通过调用Query的includeAggregator()方法添加到查询的结果中。
//创建一个用于查询的Query对象
Query query = userCache.createQuery();
Attribute<Integer> age = userCache.getSearchAttribute("age");
//查询结果中包含age的平均值和age的最大值
query.includeAggregator(age.average(), age.max());
Results results = query.execute();
List<Result> resultList = results.all();
if (resultList != null && !resultList.isEmpty()) {
//每一个查询结果Result中都会包含对查询结果的统计信息。
Result result = resultList.get(0);
//多个统计信息将会组成一个List进行返回
List<Object> aggregatorResults = result.getAggregatorResults();
Number averageAge = (Number)aggregatorResults.get(0);
Integer maxAge = (Integer)aggregatorResults.get(1);
System.out.println(averageAge + "---" + maxAge);
}
当我们的查询结果中只包含有统计信息时,我们的查询结果Results中只会有一条记录,即一个Result对象。当包含其它信息时查询结果就可能会有多条记录,而且每条记录中都会包含有对应的统计信息。
Ehcache中对于Cache的查询也是可以进行排序的,这是通过Query的addOrderBy()方法来指定的。该方法接收两个参数,第一个参数表示需要进行排序的属性Attribute,第二个参数是排序的方向Direction。Direction有两个可选值,Direction.ASCENDING和Direction.DESCENDING。当需要对多个属性进行排序时则需要调用多次addOrderBy()方法。
Attribute<String> unitNo = userCache.getSearchAttribute("unitNo");
Attribute<Integer> age = userCache.getSearchAttribute("age");
//查询结果按部门编号的升序和年龄的降序进行排列
query.addOrderBy(unitNo, Direction.ASCENDING).addOrderBy(age, Direction.DESCENDING);
Ehcache也支持对查询的缓存进行分组。这是通过Query的addGroupBy()方法来定义的,该方法接收一个Attribute作为参数,表示要对哪个Attribute进行分组,当需要对多个Attribute进行分组时,则需要调用多次addGroupBy()方法。使用分组的语法基本上跟SQL里面分组的语法是一样的,当使用分组时查询结果只能包含分组的属性和统计信息,统计信息是对分组后的情况进行统计。唯一不同的是Ehcache中查询分组时无法对分组后的情况进行筛选。
//创建一个用于查询的Query对象
Query query = userCache.createQuery();
Attribute<String> unitNo = userCache.getSearchAttribute("unitNo");
Attribute<Integer> age = userCache.getSearchAttribute("age");
//对单位编号进行分组
query.addGroupBy(unitNo);
//各单位年龄的平均值、最大值以及人数。
query.includeAggregator(age.average(), age.max(), age.count());
//查询结果中还包含单位编码
query.includeAttribute(unitNo);
Results results = query.execute();
List<Result> resultList = results.all();
if (resultList != null && !resultList.isEmpty()) {
for (Result result : resultList) {
String unitNoVal = result.getAttribute(unitNo);
//多个统计信息将会组成一个List进行返回
List<Object> aggregatorResults = result.getAggregatorResults();
Number averageAge = (Number)aggregatorResults.get(0);
Integer maxAge = (Integer)aggregatorResults.get(1);
Integer count = (Integer)aggregatorResults.get(2);
System.out.println("单位编号:" + unitNoVal + "---" + averageAge + "," + maxAge + "," + count);
}
}
默认情况下,我们的Query可以在执行后修改某些属性后继续查询。但是一旦我们调用了Query的end()方法之后我们将不能够再更改Query的一些属性。这包括调用include来定义返回结果中需要包含的信息、指定排序的属性、指定分组的属性、添加Criteria限制条件和调用maxResults()方法指定最大返回记录数。
BeanShell是用Java写的,能够对Java字符串表达式进行解释执行的一个工具。如果在实际应用中我们需要让用户来自定义查询的脚本时,我们就可以使用BeanShell来对查询脚本进行解释执行了。使用BeanShell前我们需加入BeanShell的jar包到类路径,笔者下面的示例中使用的是BeanShell2.0的第4个测试版本。
@Test
2.public void beanShell() throws EvalError {
CacheManager cacheManager = CacheManager.create();
Cache userCache = cacheManager.getCache("userCache");
User user;
for (int i=0; i<10; i++) {
user = new User(i, "name"+(i%2), 25+i);
userCache.put(new Element(user.getId(), user));
}
//BeanShell解释器,需引入BeanShell相关jar包
Interpreter interpreter = new Interpreter();
Query query = userCache.createQuery().includeValues();
//Interpreter进行计算的字符串中出现的变量都需要放入Interpreter的环境中
interpreter.set("query", query);//把query放入Interpreter环境中
//把age放入Interpreter环境中
interpreter.set("age", userCache.getSearchAttribute("age"));
String queryStr = "query.addCriteria(age.lt(30)).execute();";
//BeanShell执行字符串表达式对userCache进行查询,并返回Results
Results results = (Results)interpreter.eval(queryStr);
for (Result result : results.all()) {
System.out.println(result);
}
results.discard();
}
关于BeanShell的更多了解请访问BeanShell的官方网站www.beanshell.org。
纵观整个Ehcahce中对于Cache的查询Query,我们可以发现其基本的逻辑和规则与SQL查询是一样的。可以进行筛选、选择要查询的结果、统计、排序和分组。Ehcache中的查询也是先通过Criteria进行筛选,再进行分组和排序。