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);
}
}
順便提一下,不才小弟又要找新工作了,若有覺得適合小弟的可以聯絡交流一下。