Query By Example (QBE) 是個常用的查詢模式,Hibernate有Example Query來實踐,但JPA沒有,這對我常用的開發模式而言是個不小的困擾,所以基本上我都把JPA放在一邊,直接用Hibernate。
其實QBE的概念並不難實現,只要分析傳入的Domain Object,哪些property有值,加入查詢條件即可, 趁著覆習Spring Data的同時,就做個簡單的實作好了。
先建個ExpressionParam來儲存分析Domain Object Class後的結果,readMethod就是用來取值,而attribue則是用來取得Criteria的Path
public class ExpressionParam<T> { private String name; private Method readMethod; private SingularAttribute<T, ?> attribute; public ExpressionParam(String name, Method readMethod, SingularAttribute<T, ?> attribute) { super(); this.name = name; this.readMethod = readMethod; this.attribute = attribute; } ..... }
再來就是配合Spring-Data的Specification Query,實作一個ExampleSpecification,其中作個簡單的Cache機制,以免同樣的Domain Object Class要一直重覆分析有哪些ReadMethod。
public class ExampleSpecification<T> implements Specification<T> { private static final Logger logger = LoggerFactory.getLogger(ExampleSpecification.class); protected static final Map<Class<?>, List<ExpressionParam<?>>> classCache = Collections.synchronizedMap(new WeakHashMap<Class<?>, List<ExpressionParam<?>>>()); EntityManager entityManager; T example; public ExampleSpecification(final EntityManager entityManager, final T example) { this.entityManager = entityManager; this.example = example; } @Override public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) { List<Predicate> predicates = new ArrayList<Predicate>(); EntityType<T> entity = entityManager.getMetamodel().entity((Class<T>)example.getClass()); List<ExpressionParam<?>> params = parseReadMethod(entity); for (ExpressionParam<?> param : params) { try { Object value = param.getReadMethod().invoke(example); if (null != value && StringUtils.isNotEmpty(value.toString())) { predicates.add(cb.equal(root.get((SingularAttribute<T, ?>)param.getAttribute()), value)); } } catch (Exception e) { e.printStackTrace(); } } return predicates.isEmpty()?cb.conjunction() : cb.and(predicates.toArray(new Predicate[predicates.size()])); } protected List<ExpressionParam<?>> parseReadMethod(EntityType<T> entityType) { Class<T> clazz = (Class<T>) entityType.getClass(); if (classCache.containsKey(clazz)) { return classCache.get(clazz); } logger.info("First Parsing Read Method for Class[{}]", clazz); List<ExpressionParam<?>> methods = new ArrayList<ExpressionParam<?>>(); classCache.put(clazz, methods); PropertyDescriptor[] pds = BeanUtils.getPropertyDescriptors(example.getClass()); Set<SingularAttribute<T, ?>> atts = entityType.getDeclaredSingularAttributes(); for (SingularAttribute<T,?> sat : atts) { if (PersistentAttributeType.MANY_TO_ONE == sat.getPersistentAttributeType() || PersistentAttributeType.ONE_TO_ONE == sat.getPersistentAttributeType()) { continue; } String name = sat.getName(); Method readMethod = null; for (PropertyDescriptor pd : pds) { if (pd.getName().equals(name)) { readMethod = pd.getReadMethod(); break; } } logger.debug("Property {} - Method {}", name, readMethod); if (null != readMethod) { methods.add(new ExpressionParam<T>(name, readMethod, sat)); } } return methods; } }
由於Spring Data的Repository產生機制不容易修改(其實是我還沒找到好的切入點....)所以只好在Service Layer來達到QBE的作用,可以參考下面UserService的做法。
@Service @Transactional(readOnly=true) public class UserService { @PersistenceContext private EntityManager entityManager; @Autowired private UserJpaDao userDao; public List findByExample(User example) { ExampleSpecification es = new ExampleSpecification(entityManager, example); return userDao.findAll(es); } }
再來看一下Test的實際應用
public class UserJpaDaoTest { @Autowired private UserService userService; @Test public void test() { User example = new User(); example.setName("Bob"); this.userService.findByExample(example); } }
順便提一下,不才小弟又要找新工作了,若有覺得適合小弟的可以聯絡交流一下。
沒有留言:
張貼留言