Hibernate:深入HQL學(xué)習(xí)

2018-08-12 21:21 更新

導(dǎo)讀


HQL(Hibernate Query Language) 是面向?qū)ο蟮牟樵冋Z(yǔ)言, 它和 SQL 查詢語(yǔ)言有些相似. 在 Hibernate 提供的各種檢索方式中, HQL 是使用最廣的一種檢索方式. 它有如下功能:

  1. 在查詢語(yǔ)句中設(shè)定各種查詢條件;
  2. 支持投影查詢, 即僅檢索出對(duì)象的部分屬性;
  3. 支持分頁(yè)查詢;
  4. 支持連接查詢;
  5. 支持分組查詢, 允許使用 HAVING 和 GROUP BY 關(guān)鍵字;
  6. 提供內(nèi)置聚集函數(shù), 如 sum(), min() 和 max();
  7. 支持子查詢;
  8. 支持動(dòng)態(tài)綁定參數(shù);
  9. 能夠調(diào)用 用戶定義的 SQL 函數(shù)或標(biāo)準(zhǔn)的 SQL 函數(shù)。

HQL 查詢包括以下步驟:

  1. 獲取Hibernate Session對(duì)象。
  2. 編寫HQL語(yǔ)句
  3. 以HQL語(yǔ)句作為參數(shù),調(diào)用Session的createQuery方法創(chuàng)建查詢對(duì)象。
  4. 如果HQL語(yǔ)句包含參數(shù),則調(diào)用Query的setXxx方法為參數(shù)賦值。
  5. 調(diào)用Query對(duì)象的list()或uniqueResult()方法返回查詢結(jié)果列表(持久化實(shí)體集)

Qurey 接口支持方法鏈編程風(fēng)格, 它的 setXxx() 方法返回自身實(shí)例, 而不是 void 類型,因此可以寫類似于.setXxx().setXxx().setXxx()...樣式的語(yǔ)句。

HQL vs SQL


HQL 查詢語(yǔ)句是面向?qū)ο蟮? Hibernate 負(fù)責(zé)解析 HQL 查詢語(yǔ)句, 然后根據(jù)對(duì)象-關(guān)系映射文件中的映射信息, 把 HQL 查詢語(yǔ)句翻譯成相應(yīng)的 SQL 語(yǔ)句。HQL 查詢語(yǔ)句中的主體是域模型中的類及類的屬性。

SQL 查詢語(yǔ)句是與關(guān)系數(shù)據(jù)庫(kù)綁定在一起的。SQL 查詢語(yǔ)句中的主體是數(shù)據(jù)庫(kù)表及表的字段。

HQL實(shí)用技術(shù)


實(shí)體查詢

最簡(jiǎn)單實(shí)體查詢例子:

String hql = "from User";
Query query = session.createQuery(hql);
List<User> list = query.list();

上面的HQL語(yǔ)句將取出User的所有對(duì)應(yīng)記錄為:select user0_.U_ID as U_ID1_0_,user0_.U_NAME as U_NAME2_0_,user0_.U_AGE as U_AGE3_0_ from USERS user0_

在HQL語(yǔ)句中,本身大小寫無(wú)關(guān),但是其中出現(xiàn)的類名和屬性名必須注意大小寫區(qū)分。同時(shí),在Hibernate中,查詢的目標(biāo)實(shí)體存在繼承關(guān)系的判定,如果from User將返回所有User以及User子類的記錄。假設(shè)系統(tǒng)中存在User的兩個(gè)子類:SysAdmin和SysOperator,那么該hql語(yǔ)句返回的記錄將包含這兩個(gè)子類的所有數(shù)據(jù),即使SysAdmin和SysOperator分別對(duì)應(yīng)了不同的庫(kù)表。

Where子句: 如果我們想取出名為“Erica”的用戶記錄,可以通過Where子句加以限定(其中AS可以省略):

FROM User AS user WHERE user.name='Erica'

where子句中,我們可以通過比較操作符指定甄選條件,如: =, , <, >, <=, >=, between, not between, in ,not in, is, like等。同時(shí),在where子句中可以使用算術(shù)表達(dá)式。 幾個(gè)簡(jiǎn)單實(shí)例:

FROM User user WHERE user.age<20
FROM User user WHERE user.name IS null
FROM User user WHERE user.name LIKE 'Er%'
FROM User user WHERE (user.age % 2 = 1)
FROM User user WHERE (user.age<20) AND (user.name LIKE '%Er')

屬性查詢

有時(shí),我們需要的數(shù)據(jù)可能僅僅是實(shí)體對(duì)象的某個(gè)屬性(庫(kù)表記錄中的某個(gè)字段信息)。通過HQL可以簡(jiǎn)單的做到這一點(diǎn)。

String hql = "SELECT user.name FROM User user";
List list = session.createQuery(hql).list();
Iterator it = list.iterator();
while(it.hasNext()){
    System.out.println(it.next());
}

上例中,我們指定了只需要獲取User的name屬性。此時(shí)返回的list數(shù)據(jù)結(jié)構(gòu)中,每個(gè)條目都是一個(gè)String類型的name數(shù)據(jù)。 我們也可以通過一條HQL獲取多個(gè)屬性:

public void test() {
        String hql = "SELECT user.name,user.age FROM User user";
        List list = session.createQuery(hql).list();
        Iterator it = list.iterator();
        while(it.hasNext()){
            Object[] results = (Object[]) it.next();
            System.out.println(results[0]+","+results[1]);
        }
    }

而此時(shí),返回的list數(shù)據(jù)結(jié)構(gòu)中,每個(gè)條目都是一個(gè)對(duì)象數(shù)組(Object[]),其中依次包含了我們所獲取的屬性數(shù)據(jù)。

除此之外,我們也可以通過在HQL中動(dòng)態(tài)的構(gòu)造對(duì)象實(shí)例的方法對(duì)這些平面化的數(shù)據(jù)進(jìn)行封裝。

String hql = "SELECT new User(user.uName,user.uAge) FROM User user";
        List list = session.createQuery(hql).list();
        Iterator it = list.iterator();
        while(it.hasNext()){
            User user = (User) it.next();
            System.out.println(user);
        }

通過在HQL中動(dòng)態(tài)的構(gòu)造對(duì)象實(shí)例,我們實(shí)現(xiàn)了對(duì)查詢結(jié)果的對(duì)象化封裝。此時(shí)在查詢結(jié)果中的User對(duì)象僅僅是一個(gè)普通的Java對(duì)象,僅用于對(duì)查詢結(jié)果的封裝,除了在構(gòu)造時(shí)賦予的屬性值外,其他屬性均為未賦值狀態(tài)。同時(shí),在實(shí)體類中,要提供包含構(gòu)造屬性的構(gòu)造方法,并且順序要相同。

與此同時(shí),我們也可以在HQL的select子句中使用統(tǒng)計(jì)函數(shù)或者利用DSITINCT關(guān)鍵字,剔除重復(fù)記錄。

SELECT COUNT(*),MIN(user.age) FROM User user
SELECT DISTINCT user.name FROM User user

實(shí)體更新與刪除

String hql = "UPDATE User SET age = 18 WHERE id = 1";
        int result = session.createQuery(hql).executeUpdate();

上述代碼利用HQL語(yǔ)句實(shí)現(xiàn)了更新操作。對(duì)于單個(gè)對(duì)象的更新也許代碼量并沒有減少太多,但如果對(duì)于批量更新操作,其便捷性以及性能的提高就相當(dāng)可觀。 例如,以下代碼將所有用戶的年齡屬性更改為10

UPDATE User SET age = 18

HQL的delete子句使用同樣很簡(jiǎn)單,例如以下代碼刪除了所有年齡大于18的用戶記錄:

DELETE User WHERE age > 18

不過,需要注意的是,在HQL delete/update子句的時(shí)候,必須特別注意它們對(duì)緩存策略的影響,極有可能導(dǎo)致緩存同步上的障礙。

分組與排序

Order by子句

舉例說(shuō)明:

FROM User user ORDER BY user.name

默認(rèn)情況下是按照升序排序,當(dāng)然我們可以指定排序策略:

FROM User user ORDER BY user.name DESC

order by子句可以指定多個(gè)排序條件:

FROM User user ORDER BY user.name, user.age DESC

Group by子句

通過Group by可進(jìn)行分組統(tǒng)計(jì),如果下例中,我們通過Group by子句實(shí)現(xiàn)了同齡用戶的統(tǒng)計(jì):

SELECT COUNT(user),user.age FROM User user GROUP BY user.age

通過該語(yǔ)句,我們獲得了一系列的統(tǒng)計(jì)數(shù)據(jù)。對(duì)于Group by子句獲得的結(jié)果集而言,我們可以通過Having子句進(jìn)行篩選。例如,在上例中,我們對(duì)同齡用戶進(jìn)行了統(tǒng)計(jì),獲得了每個(gè)年齡層次中的用戶數(shù)量,假設(shè)我們只對(duì)超過10人的年齡組感興趣,可用以下語(yǔ)句實(shí)現(xiàn):

SELECT COUNT(user),user.age FROM User user GROUP BY user.age HAVING COUNT(user) > 10

參數(shù)綁定

SQL注入

在解釋參數(shù)綁定的使用時(shí),我們先來(lái)解釋一下什么是SQL注入。

SQL Injection是常見的系統(tǒng)攻擊手短,這種攻擊方式的目標(biāo)是針對(duì)由SQL字符串拼接造成的漏洞。如,為了實(shí)現(xiàn)用戶登錄功能,我們編寫了以下代碼:

FROM User user WHERE user.name='"+username+"' AND user.password='"+password+"'

從邏輯上講,該HQL并沒有錯(cuò)誤,我們根據(jù)用戶名和密碼從數(shù)據(jù)庫(kù)中讀取相應(yīng)的記錄,如果找到記錄,則認(rèn)為用戶身份合法。

假設(shè)這里的變量username和password是來(lái)自于網(wǎng)頁(yè)上輸入框的數(shù)據(jù)。現(xiàn)在我們來(lái)做個(gè)嘗試,在登錄網(wǎng)頁(yè)上輸入用戶名:"'Erica' or 'x'='x'",密碼隨意,也可以登錄成功。

此時(shí)的HQL語(yǔ)句為:

FROM User user WHERE user.name='Erica' OR 'x'='x' AND user.password='fasfas'

此時(shí),用戶名中的OR 'x'='x'被添加到了HQL并作為子句執(zhí)行,where邏輯為真,而密碼是否正確就無(wú)關(guān)緊要。

這就是SQL Injection攻擊的基本原理,而字符串拼接而成的HQL是安全漏洞的源頭。參數(shù)的動(dòng)態(tài)綁定機(jī)制可以妥善處理好以上問題。

Hibernate提供順序占位符以及引用占位符,將分別舉例說(shuō)明:

順序占位符:

String hql = "from User user WHERE user.name = ? AND user.age = ?";
        List<User> list = session.createQuery(hql).setString(0, "Erica")
                .setInteger(1, 10).list();

引用占位符:

String hql = "from User user WHERE user.uName = :name AND user.uAge = :age";
        List<User> list = session.createQuery(hql).setString("name", "Erica")
                .setInteger("age", 10).list();

我們甚至還可以用一個(gè)JavaBean來(lái)封裝查詢參數(shù)。

參數(shù)綁定機(jī)制可以使得查詢語(yǔ)法與具體參數(shù)數(shù)值相互獨(dú)立。這樣,對(duì)于參數(shù)不同,查詢語(yǔ)法相同的查詢操作,數(shù)據(jù)庫(kù)即可實(shí)施性能優(yōu)化策略。同時(shí),參數(shù)綁定機(jī)制也杜絕了參數(shù)值對(duì)查詢語(yǔ)法本身的影響,這也就是避免了SQL Injection的可能。

引用查詢

我們可能遇到過如下編碼規(guī)范:“代碼中不允許出現(xiàn)SQL語(yǔ)句”。

SQL語(yǔ)句混雜在代碼之間將破壞代碼的可讀性,并似的系統(tǒng)的可維護(hù)性降低。為了避免這樣的情況,我們通常采取將SQL配置化的方式,也就是說(shuō),將SQL保存在配置文件中。Hibernate提供了HQL可配置化的內(nèi)置支持。

我們可以在實(shí)體映射文件中,通過query節(jié)點(diǎn)定義查詢語(yǔ)句(與class節(jié)點(diǎn)同級(jí)):

<query name="queryTest"><![CDATA[FROM User user where user.uAge < 20]]></query>

需要注意的是,我們是將HQL語(yǔ)句寫入到了xml文件中,所以可能會(huì)造成沖突。例如HQL語(yǔ)句的的“<”(小于)與xml的語(yǔ)法有沖突。所以我們會(huì)用CDATA將其包裹。

之后我們可以通過session的getNamedQuery方法從配置文件中調(diào)用對(duì)應(yīng)的HQL,如:

Query query = session.getNamedQuery("queryTest");
        List<User> list = query.list();
        for(User user : list){
            System.out.println(user);
        }

關(guān)聯(lián)查詢

關(guān)于這部分的內(nèi)容,參考了很多書上的資料,但都感覺講的不夠清晰,也就是說(shuō)沒有結(jié)合到實(shí)際的情況中去。下面將按照一個(gè)視頻教程上的順序來(lái)介紹關(guān)聯(lián)查詢。

關(guān)于這部分的知識(shí)點(diǎn),是鑒于已經(jīng)對(duì)關(guān)聯(lián)連接查詢有所了解的基礎(chǔ)上,比如懂得什么是左外連接、內(nèi)連接等。 下面就開始總結(jié):

HQL迫切左外連接

LEFT JOIN FETCH關(guān)鍵字表示使用迫切左外連接策略。 首先看一下例子中的實(shí)體類,這是一個(gè)雙向1-N的映射(關(guān)于1-N的映射在之前的博客中有介紹Hibernate關(guān)系映射2:雙向1-N關(guān)聯(lián)).

實(shí)體類:

public class Department {

    private Integer id;
    private String name;
    private Set<Employee> emps = new HashSet<>();
  //省卻get和set方法
}
public class Employee {

    private Integer id;
    private String name;
    private float salary;
    private String email;

    private Department dept;
  //省去get和set方法
}

迫切左外連接

String hql = "SELECT DISTINCT d FROM Department d LEFT JOIN FETCH d.emps";
Query query = session.createQuery(hql);
List<Department> depts = query.list();
System.out.println(depts.size());
for (Department dept : depts) {
    System.out.println(dept.getName() + "-" + dept.getEmps().size());
}

上面的例子中是想得到所有的部門以及其中的員工。我們通過DISTINCT進(jìn)行去重。 注意的點(diǎn):

  • list()方法中返回的集合中存放實(shí)體對(duì)象的引用。每個(gè)Department對(duì)象關(guān)聯(lián)的Employee集合都將被初始化,存放所有關(guān)聯(lián)的Employee的實(shí)體對(duì)象
  • 查詢結(jié)果中可能會(huì)包含重復(fù)元素,可以通過DISTINCT關(guān)鍵字去重,同樣由于list中存放的實(shí)體對(duì)象的引用,所以可以通過HashSet來(lái)過濾重復(fù)對(duì)象。例如:
List<Department> depts = query.list();
depts = new ArrayList<>(new LinkedHashSet(depts));

左外連接

String hql = "FROM Department d LEFT JOIN d.emps";
Query query = session.createQuery(hql);
List<Object[]> result = query.list();
System.out.println(result);
for (Object[] objs : result) {
    System.out.println(Arrays.asList(objs));
}

注意的是:通過左外連接返回的list是一個(gè)包含Department和與之連接的Employee的object數(shù)組。所以我們還需要對(duì)數(shù)組進(jìn)行處理,并且有重復(fù)。鑒于此,我們可以通過DISTINCT進(jìn)行處理。

String hql = "SELECT DISTINCT d FROM Department d LEFT JOIN d.emps";
        Query query = session.createQuery(hql);

        List<Department> depts = query.list();
        System.out.println(depts.size());

        for (Department dept : depts) {
            System.out.println(dept.getName() + ", " + dept.getEmps().size());
        }

注意:

  • list方法返回的集合中存放的是對(duì)象數(shù)組類型
  • 根據(jù)配置文件來(lái)決定Employee集合的檢索策略,比如是fetch、lazy啊或者怎樣,還不是像迫切左外連接那樣。

我們真正進(jìn)行的過程中,一般都會(huì)使用迫切左外連接。因?yàn)槠惹凶笸膺B接只發(fā)送了一條SQL語(yǔ)句將所有信息都查出來(lái),而左外連接就算不使用集合的對(duì)象,也會(huì)進(jìn)行查詢,而當(dāng)真正使用集合的時(shí)候,又會(huì)再去查詢。所以性能上迫切左外連接要好。

子查詢

子查詢可以在HQL中利用另外一條HQL的查詢結(jié)果。 例如:

FROM User user WHERE (SELECT COUNT(*) FROM user.address) > 1

HQL中,子查詢必須出現(xiàn)在where子句中,且必須以一對(duì)圓括號(hào)包圍。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)