幾個月前,我在博問里面發了一個問題:http://q.cnblogs.com/q/64900/,但是一直沒有找到好的答案,關閉問題以后才自己解決了,在這里分享一下。
首先我重復一下場景,博問里面舉的動物的例子,這篇文章里為了和我的代碼對應,換一個例子。假設要做一個企業的人員管理系統,有各種各樣的用戶,有的身份是老板,有的身份是員工,有的身份是保安,等等,這些用戶可能有非常多非常多通用的行為,比如說修改年齡,比如說按姓名或者工號在全公司查找,並且大多數情況下我們不關心一個人的具體身份,這時候我希望采取這樣一種結構:
Person類是一個父類,擁有所有員工的公有屬性,包括員工號和姓名,Staff和Employer是兩種身份的人,都具有員工號和姓名這樣的特征,並且Employer呢還有一個座右銘,Staff還有一個薪水。
class Person implements Serializable {id, name} class Employer extends Person {motto} class Staff extends Person {salary}
這是一種繼承映射的關系,映射到數據表,就是
person表 | |
id | name |
員工號,主鍵 | 員工姓名 |
employer表 | |
id | motto |
員工號,主鍵,外鍵指向person表 | 座右銘 |
staff表 | |
id | salary |
員工號,主鍵,外鍵指向person表 | 薪水 |
我現在要查詢名字叫“張三”的所有員工,那么我可以用下面的方法查詢:
Criteria criteria = getCurrentSession().createCriteria(Person.class); criteria.add(Restrictions.eq("name", "張三")); List<Person> person = criteria.list();
此時查詢到的Person列表,所有的對象都是Employer或者Staff的具體化對象,Hibernate區分應該是哪一種對象的方式可以參考它生成的SQL語句:
Hibernate: select person0_.id as id1_3_0_, person0_.name as name2_3_0_, person0_1_.motto as motto1_0_0_, person0_2_.salary as salary1_1_0_, case when person0_1_.id is not null then 1 when person0_2_.id is not null then 2 when person0_.id is not null then 0 end as clazz_0_ from person person0_ left outer join Employer person0_1_ on person0_.id=person0_1_.id left outer join Staff person0_2_ on person0_.id=person0_2_.id where person0_.name=?
這種多態查詢非常的方便,如果只想查Staff,那么只需要寫查詢的時候,改一下class就可以,這樣就只有staff left outer join person,而沒有Employer的事情了:
Criteria criteria = getCurrentSession().createCriteria(Staff.class);
但是,毋庸置疑,當子表眾多的時候,在Person上做多態查詢,join的大量使用非常影響性能,而在這種要把Employer和Staff都查詢出來存到一個List的情況下,我們一般只關心父表里存儲的公有字段,所以我之前一直在尋找能夠只查父表的方法,但是卻沒有找到。
嘗試直接通過annotation禁用多態查詢無果后,使用一種替代的解決方案:

以前是Employer和Staff都去繼承Person,現在加一層,Person類是變成一個抽象類,不映射數據庫,PersonUntyped和PersonTyped都映射同一個數據表"person"表,需要查Employer或者Staff的時候,還像以前一樣,調用:
Criteria criteria = getCurrentSession().createCriteria(Employer.class); List<Person> ems= criteria.list(); criteria = getCurrentSession().createCriteria(Staff.class); List<Person> stas= criteria.list();
如果需要查父類對象,那么就調用:
criteria = getCurrentSession().createCriteria(PersonUntyped.class); List<Person> pers= criteria.list();
Hibernate只解析當前類的字段,所以默認情況下PersonUntyped和PersonTyped映射person表時hibernate是不認識Person中的id和name的,需要在Person類上面加MappedSuperclass注解才可以:
@MappedSuperclass public abstract class Person {
為了達到在Controller中使用同一個操作Person的接口,讓Controller中完全感受不到我們加的這一層的存在,首先和以前一樣為Employer、Staff和PersonUntyped分別建一個DAO,然后在Service中封裝:
@Service @Transactional public class PersonServiceImpl implements PersonService { private PersonDao<Person> personDao; public Person findById(int type, int id) { wireBean(type); return personDao.findById(id); } public void wireBean(int type) { WebApplicationContext wac = ContextLoader.getCurrentWebApplicationContext(); // 省略了try/trach,beanName根據type選擇是personDao、employerDao還是staffDao personDao = wac.getBean(beanName); } }
真正數據庫設計的時候,父表里還應該有一個type,每一個Service方法的第一個參數規定都是type或者是一個Person對象,可以用person.getType()來獲取到這個type,每個Service方法的第一行都要先根據type裝載personDao,如果type是1或者2,分別裝載employerDao和staffDao,如果是0,就裝載只查父表的personDao。
這樣一來這個問題就解決了,但是,當Service類擴充功能的時候,開發人員還要留心在每個函數開始都加一行裝載的調用,非常麻煩,如果能在每個方法一開始的時候自動調用裝載的代碼多么好,非常顯然,AOP可以完成這個工作,在PersonService中刪除wireBean,新建aspect包,創建一個PersonAspect類:
@Component @Aspect public class PersonServiceAspect { @Autowired private WebApplicationContext wac; @Before("execution(* org.zhangfc.demo4ssh.service.PersonServiceImpl.*(..))") public void wirePersonDao(JoinPoint joinPoint) { Object [] args = joinPoint.getArgs(); Object service = joinPoint.getTarget(); Field dao = null; try { dao = service.getClass().getDeclaredField("personDao"); if (!dao.isAccessible()) { dao.setAccessible(true); } } catch (NoSuchFieldException | SecurityException e) { return; } if (args.length == 0 || args[0].equals(0)) { setBean(dao, service, "personDao"); } if (args.length > 1 && args[0].equals(1)) { setBean(dao, service, "employerDao"); } if (args.length > 1 && args[0].equals(2)) { setBean(dao, service, "staffDao"); } } public boolean setBean(Field dao, Object service, String beanName){ try { dao.set(service, wac.getBean(beanName)); return true; } catch (BeansException | IllegalArgumentException | IllegalAccessException e) { return false; } } }
這樣,在Service中就已經看不到了不同Person對象的不同。AOP的配置不再多做描述,可以參看我以前的文章,或者下載本例源碼查看。