26 7月 2012

Query By Example In Spring-Data-JPA

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);
	}

}

順便提一下,不才小弟又要找新工作了,若有覺得適合小弟的可以聯絡交流一下。