вторник, 13 октября 2009 г.

Быстро и удобно. Hibernate Сriteria по связанным сущностям.

Hibernate как ORM очень гибкий и универсальный инструмент. При разработке на его основе испытываешь удовольствие, но как в каждом продукте есть свои неудобные моменты. Один из тех моментов, которые периодически раздражают, - это отсутствие удобного механизма по добавлению критериев по связанным объектам через Criteria API.

Рассмотрим подобную ситуацию на примере. Имеются классы Company, Address, Product, Customer связанный между собой. Данная модель может вам не нравиться, но она лишь написана для того, чтобы продемонстрировать проблему.

public class Product {
 
 public Long id;
 public String name;
 public String description;
 public Company company;
 public Set<Customer> customers = new HashSet<Customer>();

}

public class Company {

 public Long id;
 public String name;
 public Address address;
 public Set<Product> products = new HashSet<Product>();

}

public class Address {

 public Long id;
 public String name;
 public Set<Company> companies = new HashSet<Company>();

}

public class Customer {

 public Long id;
 public Set<Product> products = new HashSet<Product>();

}

Выберем пользователей, которые совершали покупки продуктов компаний, зарегистрированных на территории Москвы.

Criteria cr = session.CreateCriteria(Customer.class);
cr.createCriteria("products").createCriteria("company").createCriteria("address").add(Restrictions.like("name", "%Москва%"));

При этом существуют некоторые ограничения. Например, если вы в переменную cr попытаетесь еще добавить критерий по описанию продукта вот таким вот образом:
cr.createCriteria("products").add(Restrictions.like("description", "вафли%"))
В результате возникнет исключение "duplicate association path" :(. А что делать, если такой критерий собирается динамически?

Так как же можно упростить добавление критерия, вызвав добавление связки только один раз "products.company.address" и при этом еще избежать дублирования при добавлении критерия "products"?

"Рождение" SeparatorCriteriaHelper

Поискав в инете, наткнулся на парочку постов, частично решавших данную проблему с использованием HQL, но таких решений с Criteria API не нашел. Возможно это из-за того, что нет возможности перейти/поискать по дереву критериев. Подумав еще чуть-чуть, принял решение, что необходимо написать такой хелпер.

Вот так и "родился" SeparatorCriteriaHelper.

import java.util.Hashtable;
import java.util.Map;

import org.hibernate.Criteria;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Order;
import org.hibernate.sql.JoinFragment;

/**
 * Helper для создания критерия
 * @author s.melnichenko
 */
public class SeparatorCriteriaHelper {
 
 public static final String CRITERIA_SEPARATOR = "\\.";
 
 /**
  * Создает helper
  * @param criteria - основной критерий
  */
 public SeparatorCriteriaHelper(Criteria criteria){
  if (criteria==null){
   throw new IllegalArgumentException("Argument criteria can`t be null!");
  }
  
  this.cr = criteria;
  this.separator = CRITERIA_SEPARATOR;
 }
 
 /**
  * Создает helper
  * @param criteria - основной критерий
  * @param separator - разделитель в строке критериев
  */
 public SeparatorCriteriaHelper(Criteria criteria, String separator){
  if (criteria==null){
   throw new IllegalArgumentException("Argument criteria can`t be null!");
  }
  
  this.cr = criteria;
  this.separator = separator;
 }
 
 private final String separator;
 private final Criteria cr;
 private final Map<String, Criteria> criteriaList = new Hashtable<String, Criteria>();

 public Criteria getCriteria(){
  return cr;
 }
 
 /**
  * Возвращает разделитель в строке притериев
  * @return разделитель в строке критериев
  */
 public String getSeparator(){
  return separator;
 }
 
 /**
  * Добавляет критерий по заданому пути
  * @param path - путь критерия
  * @param arg0 - элемент критерион
  * @return результирующий критерий
  */
 public Criteria add(String path, Criterion arg0){
  Criteria crit = getOrCreateCriteriaByPath(path);
  return crit.add(arg0);
 }
 
 /**
  * Добавляет сортировку по заданому пути
  * @param path - путь критерия
  * @param arg0 - сортировка по полю
  * @return результирующий критерий
  */
 public Criteria addOrder(String path, Order arg0){
  Criteria crit = getOrCreateCriteriaByPath(path);
  return crit.addOrder(arg0);
 }
 
 /**
  * Возвращает или создает критерий по заданному пути
  * @param path - путь критерия
  * @return критерий по заданному пути
  */
 public Criteria getOrCreateCriteriaByPath(String path){
  return getOrCreateCriteriaByPath(path, JoinFragment.LEFT_OUTER_JOIN);
 }
 
 /**
  * Возвращает или создает критерий по заданному пути
  * @param path - путь критерия
  * @param joinType - тип связывания критерия
  * @return критерий по заданному пути
  */
 public Criteria getOrCreateCriteriaByPath(String path, int joinType){
  if (path==null || path.trim().equals("")){
   return cr;
  }
  path = path.trim();
  
  if (criteriaList.containsKey(path)) {
   return criteriaList.get(path);
  } else {
   return getOrCreateCriteriaByPath(path.split(separator), joinType);
  }
 }
 
 /**
  * Возвращает или создает критерий по заданному пути
  * @param path - путь критерия
  * @param joinType - тип связывания критерия
  * @return критерий по заданному пути
  */
 public Criteria getOrCreateCriteriaByPath(String[] path, int joinType) {
  
  Criteria result = cr;
  String currentPath = "";
  
  for(String el: path) {
   
   if (el==null || el.trim().equals("")){
    throw new IllegalArgumentException("Element criteria path can`t be empty");
   }
   
   currentPath = String.format("%s.%s", currentPath, el.trim());
   if (criteriaList.containsKey(currentPath)){
    result = criteriaList.get(currentPath);
   } else {
    //JoinFragment
    result = result.createCriteria(el.trim(), joinType);
    criteriaList.put(currentPath, result);
   }
  }  
  return result;}}

Работа с SeparatorCriteriaHelper

Добавление критерия описанного выше при использовании данного класса будет выглядеть следующим образом:

SeparatorCriteriaHelper helper = new SeparatorCriteriaHelper(session.CreateCriteria(Customer.class));
helper.add("products.company.address",Restrictions.like("name", "%Москва%"));

Работает, ошибок не возникает, если необходимо, то можно получить критерий заданного уровня. Прелесть хелпера заключается еще в том, что его очень легко приспособить для добавления критерия в динамике.

Комментариев нет:

Отправить комментарий