我们将复杂的对象存储在Hazelcast映射中,不仅需要根据关键字而且还可以根据这些复杂对象的内容来搜索对象。为了避免对性能造成太大影响,我们在这些搜索字词上使用了索引。
我们还使用spring-data-hazelcast,它提供的存储库使我们可以使用findByAbcXyz()类型的语义查询。对于某些更复杂的查询,我们使用@Query批注(spring-data-hazelcast在内部将其转换为SqlPredicates)。
现在我们遇到了一个问题,在某些情况下,即使我们可以验证搜索对象实际上是否存在于地图中,这些基于@Query的搜索方法也不会返回任何值。
我设法通过核心hazelcast重现了这个问题(即不使用spring-data-hazelcast)。
这是我们的对象结构:
BetriebspunktKey.java
public class BetriebspunktKey implements Serializable {
private Integer uicLand;
private Integer nummer;
public BetriebspunktKey(final Integer uicLand, final Integer nummer) {
this.uicLand = uicLand;
this.nummer = nummer;
}
public Integer getUicLand() {
return uicLand;
}
public Integer getNummer() {
return nummer;
}
}
Betriebspunkt.java
public class Betriebspunkt implements Serializable {
private BetriebspunktKey key;
private List<BetriebspunktVersion> versionen;
public Betriebspunkt(final BetriebspunktKey key, final List<BetriebspunktVersion> versionen) {
this.key = key;
this.versionen = versionen;
}
public BetriebspunktKey getKey() {
return key;
}
}
BetriebspunktVersion.java
public class BetriebspunktVersion implements Serializable {
private List<BetriebspunktKey> zusatzbetriebspunkte;
public BetriebspunktVersion(final List<BetriebspunktKey> zusatzbetriebspunkte) {
this.zusatzbetriebspunkte = zusatzbetriebspunkte;
}
}
在我的主文件中,我正在设置hazelcast:
Config config = new Config();
final MapConfig mapConfig = config.getMapConfig("points");
mapConfig.addMapIndexConfig(new MapIndexConfig("versionen[any].zusatzbetriebspunkte[any].nummer", false));
HazelcastInstance instance = Hazelcast.newHazelcastInstance(config);
IMap<BetriebspunktKey, Betriebspunkt> map = instance.getMap("points");
我还在为以后的搜索准备标准:
Predicate equalPredicate = Predicates.equal("versionen[any].zusatzbetriebspunkte[any].nummer", 53090);
Predicate sqlPredicate = new SqlPredicate("versionen[any].zusatzbetriebspunkte[any].nummer=53090");
接下来,我要创建两个对象,一个对象具有“完整深度”的信息,另一个对象不包含任何“ zusatzbetriebspunkte”:
final Betriebspunkt abc = new Betriebspunkt(
new BetriebspunktKey(80, 166),
Collections.singletonList(new BetriebspunktVersion(
Collections.singletonList(new BetriebspunktKey(80, 53090))
))
);
final Betriebspunkt def = new Betriebspunkt(
new BetriebspunktKey(83, 141),
Collections.singletonList(new BetriebspunktVersion(
Collections.emptyList()
))
);
在这里,事情变得有趣起来。如果我首先将“完整”对象插入到地图中,则使用EqualPredicate和SqlPredicate的搜索均有效:
map.put(abc.getKey(), abc);
map.put(def.getKey(), def);
Collection<Betriebspunkt> equalResults = map.values(equalPredicate);
Collection<Betriebspunkt> sqlResults = map.values(sqlPredicate);
assertEquals(1, equalResults.size()); // contains "abc"
assertEquals(1, sqlResults.size()); // contains "abc"
但是,如果我以相反的顺序将对象插入到映射中(即,首先是“部分”对象,然后是“完整”对象),则只有EqualPredicate可以正常工作,无论对象的内容是什么,SqlPredicate都会返回一个空列表。地图或搜索条件。
map.put(abc.getKey(), abc);
map.put(def.getKey(), def);
Collection<Betriebspunkt> equalResults = map.values(equalPredicate);
Collection<Betriebspunkt> sqlResults = map.values(sqlPredicate);
assertEquals(1, equalResults.size()); // contains "abc"
assertEquals(1, sqlResults.size()); // --> this fails, it returns en empty list
这种行为的原因是什么?它看起来像hazelcast代码中的错误。
最佳答案
失败的原因
经过大量调试,我找到了此问题的原因。确实可以在hazelcast代码中找到原因。
将值输入到hazelcast映射中时,将调用DefaultRecordStore.putInternal
。在此方法末尾,将调用DefaultRecordStore.saveIndex
,它查找相应的索引,然后调用Indexes.saveEntryIndex
。该方法遍历每个索引并调用InternalIndex.saveEntryIndex
(或更确切地说,其实现IndexImpl.saveEntryIndex
。该方法的有趣之处在于以下几行:
if (this.converter == null || this.converter == TypeConverters.NULL_CONVERTER) {
this.converter = entry.getConverter(this.attributeName);
}
显然,当第一个元素放入映射中时,每个索引都会存储一个转换器类。查看
QueryableEntry.getConverter
会说明会发生什么: TypeConverter getConverter(String attributeName) {
Object attribute = this.getAttributeValue(attributeName);
if (attribute == null) {
return TypeConverters.NULL_CONVERTER;
} else {
AttributeType attributeType = this.extractAttributeType(attributeName, attribute);
return attributeType == null ? TypeConverters.IDENTITY_CONVERTER : attributeType.getConverter();
}
}
第一次插入“完整”对象时,
extractAttributeType()
将遵循索引定义“ versionen [any] .zusatzbetriebspunkte [any] .nummer”的“路径”,并发现nummer
是整数类型,因此是TypeConverters .IntegerConverter将被返回并存储。第一次插入“部分”对象时,“ zusatzbetriebspunkte [any]”为空,并且
extractAttributeType
无法找到nummer
具有的类型,因此它返回null,这意味着使用TypeConverters.IdentityConverter。而且,每当插入“完整”元素时,就使用
nummer
作为键将条目写入索引映射,即索引映射的类型为Map。写地图就这么多。现在让我们看看如何从地图读取数据。调用
map.values(predicate)
时,我们最终将进入QueryRunner.runUsingGlobalIndexSafely
,其中包含一行:Collection<QueryableEntry> entries = indexes.query(predicate);
依次调用一些样板代码后
Set<QueryableEntry> result = indexAwarePredicate.filter(queryContext);
对于这两个谓词,我们最终将进入
IndexImpl.getRecords()
,其外观如下: public Set<QueryableEntry> getRecords(Comparable attributeValue) {
long timestamp = this.stats.makeTimestamp();
if (this.converter == null) {
this.stats.onIndexHit(timestamp, 0L);
return new SingleResultSet((Map)null);
} else {
Set<QueryableEntry> result = this.indexStore.getRecords(this.convert(attributeValue));
this.stats.onIndexHit(timestamp, (long)result.size());
return result;
}
}
关键调用是
this.convert(attributeValue)
,其中attributeValue
是谓词的value
。如果我们比较两个谓词,我们可以看到EqualPredicate有两个成员:
attributeName = "versionen[any].zusatzbetriebspunkte[any].nummer"
value = {Integer} 53090
SqlPredicate包含初始字符串(我们将其传递给其构造函数),但在构造时也会对其进行解析并将其映射到内部EqualPredicate(在评估该谓词时最终将其使用并传递给上述getRecords()):
sql = "versionen[any].zusatzbetriebspunkte[any].nummer=53090"
predicate = {EqualPredicate}
attributeName = "versionen[any].zusatzbetriebspunkte[any].nummer"
value = {String} "53090"
这就解释了为什么手动创建的EqualPredicate在两种情况下都起作用的原因:其值为整数。当传递给转换器时,无论是IntegerConverter还是IdentityConverter都没有关系,因为两者都将返回整数,然后可以将其用作索引映射中的键(使用整数作为键)。
但是,对于SqlPredicate,该值为字符串。如果将其传递给IntegerConverter,则会将其转换为其相应的整数值,并访问索引映射。如果将其传递给IdentityConverter,则转换将返回该字符串,并且尝试使用字符串访问索引映射将永远找不到任何结果。
可能的解决方案
我们如何解决这个问题?我看到几种可能性:
在启动过程中向我们的地图中插入一个“完全构建”的虚拟值,以确保转换器已正确初始化。虽然可行,但它很丑陋且不便于维护
避免使用SqlPredicate并使用基于整数的EqualPredicate。使用spring-data-hazelcast时,这不是一个选项,因为它总是将基于@Query的搜索转换为SqlPredicates。我们当然可以直接使用hazelcast并避开spring-data包装器,但是虽然可以正常工作,但这意味着有两种访问hazelcast的方式,这种方式也不易维护
使用hazelcast的ValueExtractor类。这是一种优雅的解决方案,既可以在本地使用,也可以使用spring-data-hazelcast进行工作。我将概述一下外观:
首先,我们需要实现一个值提取器,该值提取器以适合我们的形式返回Betriebspunkt的所有zusatzbetriebspunkte
public class BetriebspunktExtractor extends ValueExtractor<Betriebspunkt, String> implements Serializable {
@Override
public void extract(final Betriebspunkt betriebspunkt, final String argument, final ValueCollector valueCollector) {
betriebspunkt.getVersionen().stream()
.map(BetriebspunktVersion::getZusatzbetriebspunkte)
.flatMap(List::stream)
.map(zbp -> zbp.getUicLand() + "_" + zbp.getNummer())
.forEach(valueCollector::addObject);
}
}
您会注意到,我不仅返回了
nummer
字段,而且还包含了uicLand
字段,这是我们真正想要的东西,但是无法使用“ ... [any] ...”表示法来工作。如果我们想要与上面概述的行为完全相同的行为,我们当然只能返回数字。现在我们需要稍微修改我们的hazelcast配置:
Config config = new Config();
final MapConfig mapConfig = config.getMapConfig("points");
//mapConfig.addMapIndexConfig(new MapIndexConfig("versionen[any].zusatzbetriebspunkte[any].nummer", false));
mapConfig.addMapIndexConfig(new MapIndexConfig("zusatzbetriebspunkt", false));
mapConfig.addMapAttributeConfig(new MapAttributeConfig("zusatzbetriebspunkt", BetriebspunktExtractor.class.getName()));
您会注意到,不再需要使用“ ... [any] ...”符号的“长”索引定义。
现在,我们可以使用此“伪属性”来查询我们的值,而将对象添加到地图的顺序无关紧要:
Predicate keyPredicate = Predicates.equal("zusatzbetriebspunkt", "80_53090");
Collection<Betriebspunkt> keyResults = map.values(keyPredicate);
assertEquals(1, keyResults.size()); // always contains "abc"
在我们的spring-data-hazelcast存储库中,我们现在可以执行以下操作:
@Query("zusatzbetriebspunkt=%d_%d")
List<StammdatenBetriebspunkt> findByZusatzbetriebspunkt(Integer uicLand, Integer nummer);
如果您不需要使用spring-data-hazelcast,则可以直接返回BetriebspunktKey,然后在谓词中使用它,而不是将字符串返回给ValueCollector。那将是最干净的解决方案:
public class BetriebspunktExtractor extends ValueExtractor<Betriebspunkt, String> implements Serializable {
@Override
public void extract(final Betriebspunkt betriebspunkt, final String argument, final ValueCollector valueCollector) {
betriebspunkt.getVersionen().stream()
.map(BetriebspunktVersion::getZusatzbetriebspunkte)
.flatMap(List::stream)
//.map(zbp -> zbp.getUicLand() + "_" + zbp.getNummer())
.forEach(valueCollector::addObject);
}
}
接着
Predicate keyPredicate = Predicates.equal("zusatzbetriebspunkt", new BetriebspunktKey(80, 53090));
但是,要使其正常工作,BetriebspunktKey需要实现
Comparable
,并且还必须提供自己的equals
和hashCode
方法。
关于java - Hazelcast无法与SqlPredicate和可选字段上的索引一起正常使用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54095774/