Поиск элементов в Hazelcast Map с помощью ValueExtractor
Hazelcast предоставляет разработчику довольно удобные и мощные средства поиска элементов в IMap, называемые Distributed Queries. К сожалению официальная документация описывает только базовые варианты использования и совсем мало информации о возможностях Value Extractors.
В данной статье постараюсь описать свой опыт использования Value Exctractors для решения задач поиска в IMap, когда значением в IMap является коллекция объектов и нам нужно найти запись в IMap у которой есть заданный элемент в коллекции.
Итак, начнем с варианта, когда у нас есть структура для хранения корзины покупателя для какого-нибудь интернет магазина, например:
IMap<String, List> bucket; //key - имя пользователя, value - список товаров.
bucket.put("Mikhail", Arrays.asList("iphone", "ibook"));
bucket.put("Archi", Arrays.asList("iphone", "ipizza"));
bucket.put("Anna", Arrays.asList("imirror", "iphone"));
И мы хотим найти всех пользователей, у которых в корзине есть iphone. Для этого Hazelcast предоставляет механизм называемый ValueExtractor, данный механизм является распределенным, то есть данные обрабатываются для каждого узла независимо, что эффективнее с точки зрения производительности, чем фильтрация данных на клиенте.
Чтобы воспользоваться Value Extractor нужно сделать 2 вещи:
- Описать класс расширяющий ValueExtractor, который будет реализовывать логику обработки и извлечения нужных данных из значения записи в IMap, в нашем случае из списка товаров.
- Зарегистрировать данный класс в конфигурации для IMap как custom attribute.
Далее станет возможным использовать данный value extractor в предикатах для поиска данных в IMap.
Итак, создадим класс и реализуем нужную нам логику извлечения данных в методе extract:
public class BucketGoodsValueExtractor extends ValueExtractor<List<String>, String> {
@Override
public void extract(List<String> goods, String arg, ValueCollector valueCollector) {
if ("any".equalsIgnoreCase(arg)) {
for (String item : goods) {
valueCollector.addObject(item);
}
}
}
}
Метод extract принимает три параметра:
Первый параметр - это само значение записи (entry.value) в IMap, которое hazelcast передаст в этот метод для каждого ключа. В нашем случае это список товаров в корзине у пользователя.
Второй параметр - это extraction argument который можно задать в предикате в квадратных скобках и использовать его в value extractor для дополнительной гибкости. В нашем случае с его помощью задается критерий, что любой элемент списка может удовлетворять предикату. Кроме этого мы можем реализовать и другие критерии, например, первый или последний элемент и так далее:
public class BucketGoodsValueExtractor extends ValueExtractor<List<String>, String> {
@Override
public void extract(List<String> goods, String arg, ValueCollector valueCollector) {
if ("first".equalsIgnoreCase(arg)) {
if (goods.size() > 0) {
valueCollector.addObject(goods.get(0));
}
} else if ("last".equalsIgnoreCase(arg)) {
if (goods.size() > 0) {
valueCollector.addObject(goods.get(goods.size() - 1));
}
} else if ("any".equalsIgnoreCase(arg)) {
for (String item : goods) {
valueCollector.addObject(item);
}
}
}
}
Третий параметр метода extract - это value collector, контейнер для результатов, для которого будет применяться сам предикат. В нашем случае мы помещаем туда нужные нам элементы из списка товаров, в зависимости от критерия: первый, последний или все элементы списка.
Зарегистрируем custom attribute с именем “bucketGoods” и нашим value extractor’ом :
Config cfg = new Config();
MapConfig bucketMapConfig = new MapConfig("bucket");
bucketMapConfig.addMapAttributeConfig(new MapAttributeConfig("bucketGoods", BucketGoodsValueExtractor.class.getName()));
cfg.addMapConfig(bucketMapConfig);
HazelcastInstance hazelcast = Hazelcast.newHazelcastInstance(cfg);
Здесь указан пример конфигурации из исходно кода, кроме того можно воспользоваться xml конфигурацией, подробности можно найти в документации.
Теперь добавим для примера нескольких пользователей:
IMap<String, List<String>> bucket = hazelcast.getMap("bucket");
bucket.put("Mikhail", Arrays.asList("iphone", "ibook"));
bucket.put("Archi", Arrays.asList("ibottle", "ipizza"));
bucket.put("Anna", Arrays.asList("imirror", "iphone"));
И вот, мы можем найти пользователей у кого в корзине есть “iphone”
Predicate withIPhone = Predicates.equal("bucketGoods[any]", "iphone");
List<String> usersWithIPhone = bucket.entrySet(withIPhone).stream().map(Map.Entry::getKey).collect(Collectors.toList());
System.out.println(usersWithIPhone); // [Anna, Mikhail]
Обратите внимание, мы задали предикат equals с нашим Value Extractor, зарегистрированным как “bucketGoods” и в квадратных скобках указали критерий “any”:
Predicates.equal("bucketGoods[any]", "iphone")
Таким же образом можем найти пользователей у кого “iphone” добавлен первым:
Predicate withIPhoneFirst = Predicates.equal("bucketGoods[first]", "iphone");
List<String> usersWithIPhoneFirst = bucket.entrySet(withIPhoneFirst).stream().map(Map.Entry::getKey).collect(Collectors.toList());
System.out.println(usersWithIPhoneFirst); // [Mikhail]
или последним:
Predicate withIPhoneLast = Predicates.equal("bucketGoods[last]", "iphone");
List<String> usersWithIPhoneLast = bucket.entrySet(withIPhoneLast).stream().map(Map.Entry::getKey).collect(Collectors.toList());
System.out.println(usersWithIPhoneLast); // [Anna]
Теперь более сложный вариант.
Добавим цену и категорию для товаров в корзине. Простой класс для товара в корзине:
public class Goods implements Serializable {
private final String goodsName;
private final int goodsPrice;
private final String category;
public Goods(String goodsName, int goodsPrice, String category) {
this.goodsName = goodsName;
this.goodsPrice = goodsPrice;
this.category = category;
}
public String getGoodsName() {
return goodsName;
}
public int getGoodsPrice() {
return goodsPrice;
}
public String getCategory() {
return category;
}
}
Опишем Value Extractor, который будет извлекать сумму товаров, для заданной с помощью extraction argument категории, либо общую если категория не задана:
public class BucketSumPriceValueExtractor extends ValueExtractor<List<Goods>, String> {
@Override
public void extract(List<Goods> goods, String arg, ValueCollector collector) {
if (arg != null) {
collector.addObject(
goods.stream()
.filter(item -> arg.equalsIgnoreCase(item.getCategory()))
.mapToInt(Goods::getGoodsPrice)
.sum()
);
} else {
collector.addObject(
goods.stream()
.mapToInt(Goods::getGoodsPrice)
.sum()
);
}
}
}
Зарегистрируем custom attribute “bucketSumPrice” и добавми несколько записей в IMap
Config cfg = new Config();
MapConfig bucketMapConfig = new MapConfig("bucket");
bucketMapConfig.addMapAttributeConfig(new MapAttributeConfig("bucketSumPrice", BucketSumPriceValueExtractor.class.getName()));
cfg.addMapConfig(bucketMapConfig);
HazelcastInstance hazelcast = Hazelcast.newHazelcastInstance(cfg);
IMap<String, List<Goods>> bucket = hazelcast.getMap("bucket");
bucket.put("Mikhail", Arrays.asList(
new Goods("iphone", 10, "device"),
new Goods("ipad", 20, "device"),
new Goods("ipizza", 2, "food")));
bucket.put("Archi", Arrays.asList(
new Goods("ibottle", 3, "dishes"),
new Goods("ipizza", 2, "food")));
bucket.put("Anna", Arrays.asList(
new Goods("imirror", 5, "furniture"),
new Goods("iphone", 10, "device")));
И выберем пользователей у которых сумма цен товаров в корзине больше 10:
Predicate totalSumGrater10 = Predicates.greaterThan("bucketSumPrice", 10);
List<String> usersWithTotalSumGrater10 = bucket.entrySet(totalSumGrater10).stream().map(Map.Entry::getKey).collect(Collectors.toList());
System.out.println(usersWithTotalSumGrater10); // [Anna, Mikhail]
Или пользователей у которых сумма цен товаров с категорией “device” больше 20:
Predicate devicesSumGrater10 = Predicates.greaterThan("bucketSumPrice[device]", 10);
List<String> usersWithDevicesSumGrater10 = bucket.entrySet(devicesSumGrater10).stream().map(Map.Entry::getKey).collect(Collectors.toList());
System.out.println(usersWithDevicesSumGrater10); // [Mikhail]
Как мы видим Value Extractor и Custom Attributes в Hazelcast предоставляют гибкий и распределенный механизм для извлечения данных, которые недоступны как атрибуты объекта, а также может быть использован для извлечения и поиска данных в значениях, которые являются коллекциями. Этот механизм отличная альтернатива для фильтрации данных на стороне клиента.