深入HQL學習以及HQL和SQL的區別

HQL(Hibernate Query Language) 是面向對象的查詢語言, 它和 SQL 查詢語言有些相似. 在 Hibernate 提供的各種檢索方式中, HQL 是使用最廣的一種檢索方式. 它有如下功能:

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

HQL 查詢包括以下步驟:

  1. 獲取Hibernate Session對象。
  2. 編寫HQL語句
  3. 以HQL語句作爲參數,調用Session的createQuery方法創建查詢對象。
  4. 如果HQL語句包含參數,則調用Query的setXxx方法爲參數賦值。
  5. 調用Query對象的list()或uniqueResult()方法返回查詢結果列表(持久化實體集)

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

HQL vs SQL


HQL 查詢語句是面向對象的, Hibernate 負責解析 HQL 查詢語句, 然後根據對象-關係映射文件中的映射信息, 把 HQL 查詢語句翻譯成相應的 SQL 語句。HQL 查詢語句中的主體是域模型中的類及類的屬性。

SQL 查詢語句是與關係數據庫綁定在一起的。SQL 查詢語句中的主體是數據庫表及表的字段。

HQL實用技術


實體查詢

最簡單實體查詢例子:

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

上面的HQL語句將取出User的所有對應記錄爲: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語句中,本身大小寫無關,但是其中出現的類名和屬性名必須注意大小寫區分。同時,在Hibernate中,查詢的目標實體存在繼承關係的判定,如果from User將返回所有User以及User子類的記錄。假設系統中存在User的兩個子類:SysAdmin和SysOperator,那麼該hql語句返回的記錄將包含這兩個子類的所有數據,即使SysAdmin和SysOperator分別對應了不同的庫表。

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

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

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

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')

屬性查詢

有時,我們需要的數據可能僅僅是實體對象的某個屬性(庫表記錄中的某個字段信息)。通過HQL可以簡單的做到這一點。

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屬性。此時返回的list數據結構中,每個條目都是一個String類型的name數據。 我們也可以通過一條HQL獲取多個屬性:

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

而此時,返回的list數據結構中,每個條目都是一個對象數組(Object[]),其中依次包含了我們所獲取的屬性數據。

除此之外,我們也可以通過在HQL中動態的構造對象實例的方法對這些平面化的數據進行封裝。

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中動態的構造對象實例,我們實現了對查詢結果的對象化封裝。此時在查詢結果中的User對象僅僅是一個普通的Java對象,僅用於對查詢結果的封裝,除了在構造時賦予的屬性值外,其他屬性均爲未賦值狀態。同時,在實體類中,要提供包含構造屬性的構造方法,並且順序要相同。

與此同時,我們也可以在HQL的select子句中使用統計函數或者利用DSITINCT關鍵字,剔除重複記錄。

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

實體更新與刪除

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

上述代碼利用HQL語句實現了更新操作。對於單個對象的更新也許代碼量並沒有減少太多,但如果對於批量更新操作,其便捷性以及性能的提高就相當可觀。 例如,以下代碼將所有用戶的年齡屬性更改爲10

UPDATE User SET age = 18

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

DELETE User WHERE age > 18

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

分組與排序

Order by子句

舉例說明:

FROM User user ORDER BY user.name

默認情況下是按照升序排序,當然我們可以指定排序策略:

FROM User user ORDER BY user.name DESC

order by子句可以指定多個排序條件:

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

Group by子句

通過Group by可進行分組統計,如果下例中,我們通過Group by子句實現了同齡用戶的統計:

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

通過該語句,我們獲得了一系列的統計數據。對於Group by子句獲得的結果集而言,我們可以通過Having子句進行篩選。例如,在上例中,我們對同齡用戶進行了統計,獲得了每個年齡層次中的用戶數量,假設我們只對超過10人的年齡組感興趣,可用以下語句實現:

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

參數綁定

SQL注入

在解釋參數綁定的使用時,我們先來解釋一下什麼是SQL注入。

SQL Injection是常見的系統攻擊手短,這種攻擊方式的目標是針對由SQL字符串拼接造成的漏洞。如,爲了實現用戶登錄功能,我們編寫了以下代碼:

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

從邏輯上講,該HQL並沒有錯誤,我們根據用戶名和密碼從數據庫中讀取相應的記錄,如果找到記錄,則認爲用戶身份合法。

假設這裏的變量username和password是來自於網頁上輸入框的數據。現在我們來做個嘗試,在登錄網頁上輸入用戶名:"'Erica' or 'x'='x'",密碼隨意,也可以登錄成功。

此時的HQL語句爲:

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

此時,用戶名中的OR 'x'='x'被添加到了HQL並作爲子句執行,where邏輯爲真,而密碼是否正確就無關緊要。

這就是SQL Injection攻擊的基本原理,而字符串拼接而成的HQL是安全漏洞的源頭。參數的動態綁定機制可以妥善處理好以上問題。

Hibernate提供順序佔位符以及引用佔位符,將分別舉例說明:

順序佔位符:

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

我們甚至還可以用一個JavaBean來封裝查詢參數。

參數綁定機制可以使得查詢語法與具體參數數值相互獨立。這樣,對於參數不同,查詢語法相同的查詢操作,數據庫即可實施性能優化策略。同時,參數綁定機制也杜絕了參數值對查詢語法本身的影響,這也就是避免了SQL Injection的可能。

引用查詢

我們可能遇到過如下編碼規範:“代碼中不允許出現SQL語句”。

SQL語句混雜在代碼之間將破壞代碼的可讀性,並似的系統的可維護性降低。爲了避免這樣的情況,我們通常採取將SQL配置化的方式,也就是說,將SQL保存在配置文件中。Hibernate提供了HQL可配置化的內置支持。

我們可以在實體映射文件中,通過query節點定義查詢語句(與class節點同級):

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

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

之後我們可以通過session的getNamedQuery方法從配置文件中調用對應的HQL,如:

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

關聯查詢

關於這部分的內容,參考了很多書上的資料,但都感覺講的不夠清晰,也就是說沒有結合到實際的情況中去。下面將按照一個視頻教程上的順序來介紹關聯查詢。

關於這部分的知識點,是鑑於已經對關聯連接查詢有所瞭解的基礎上,比如懂得什麼是左外連接、內連接等。 下面就開始總結:

HQL迫切左外連接

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

實體類:

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進行去重。 注意的點:

  • list()方法中返回的集合中存放實體對象的引用。每個Department對象關聯的Employee集合都將被初始化,存放所有關聯的Employee的實體對象
  • 查詢結果中可能會包含重複元素,可以通過DISTINCT關鍵字去重,同樣由於list中存放的實體對象的引用,所以可以通過HashSet來過濾重複對象。例如:
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是一個包含Department和與之連接的Employee的object數組。所以我們還需要對數組進行處理,並且有重複。鑑於此,我們可以通過DISTINCT進行處理。

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方法返回的集合中存放的是對象數組類型
  • 根據配置文件來決定Employee集合的檢索策略,比如是fetch、lazy啊或者怎樣,還不是像迫切左外連接那樣。

我們真正進行的過程中,一般都會使用迫切左外連接。因爲迫切左外連接只發送了一條SQL語句將所有信息都查出來,而左外連接就算不使用集合的對象,也會進行查詢,而當真正使用集合的時候,又會再去查詢。所以性能上迫切左外連接要好。

子查詢

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

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

HQL中,子查詢必須出現在where子句中,且必須以一對圓括號包圍。

發佈了63 篇原創文章 · 獲贊 148 · 訪問量 45萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章