【JavaEE】Hibernate繼承映射,不用多態查詢只查父表的方法


幾個月前,我在博問里面發了一個問題: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的配置不再多做描述,可以參看我以前的文章,或者下載本例源碼查看。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM