1. 程式人生 > >雲客Drupal8原始碼分析之實體查詢entityQuery

雲客Drupal8原始碼分析之實體查詢entityQuery

通過本系列前面內容的學習你已經知道實體在資料庫中是如何儲存的,簡單來說儲存實體的資料庫表分為兩大類,專用表和共享表,共享表必有基本表,可能有版本表、資料表、版本資料表,總之大多數情況下一個完整的實體被儲存在多張表中,比如我們在後臺建立一個內容型別,她的資料至少儲存在六張表中,這帶來一個問題:當開發者需要找出滿足特點條件的實體時,如產品名包含特定關鍵詞且價格在某一區間的產品,如果直接採用資料庫元件進行查詢,那麼要將本來高階抽象的實體概念降低到儲存層面級別,需要知道儲存結構和具體的表列名等,往往還要寫多個結聯JOIN查詢語句,這是拙劣的,又比如有些欄位是引用型別的,假如要查詢滿足某個條件的使用者釋出的文章,這種情況牽扯到多個實體型別,複雜度進一步加大,然而在系統中關於實體的查詢隨處可見,因此係統為我們提供了統一的實體查詢方式,稱為實體查詢Entity query,她建立在資料庫元件及實體儲存之上,封裝了內部細節,使用者只需要簡單按介面使用即可,不必關注儲存結構和複雜的查詢構造,有兩種型別的實體查詢:
常規實體查詢Query:
又可稱為基本實體查詢,可以依據實體屬性(即實體欄位物件)不同的值設定條件,找到滿足條件的實體,如一個實體有價格屬性,可以通過設定在某價格區間這個條件找到特定的實體,常規實體查詢可返回滿足條件的實體id或實體數量。
實體聚集查詢QueryAggregate:
聚集aggregate又可翻譯為聚合、合計、彙總(在許多資料中使用聚集一詞,因此本系列稱為聚集),源於sql語句的聚集函式,在sql中常用的聚集函式有: 求平均值、求行總數、最大值、最小值、求和,在sql中這些函式作用於查詢結果集,在實體查詢中針對實體集查詢,可進行如某產品的平均價格之類的查詢,聚集查詢是建立在常規查詢之上的,因此實體聚集查詢繼承自常規實體查詢。

資料庫元件知識回顧:

這裡做一個簡單的回顧和補充,重點講解需要用到的知識,你可以閱讀本系列資料庫主題了解更多,如果你對資料庫掌握還不足,基本的如不清楚分組查詢、聚集函式、表示式欄位、別名、聯結等等建議你先打好基礎,在補充說明裡面雲客給大家推薦了一些學習資料。

得到一個數據庫連線可以使用以下方法:

    $con=\Drupal::database(); //獲取配置中$databases['default']['default']表示的連結,這對於大多數只有一個數據庫的站點而言是最常用的,全域性獲取  
    $con=\Drupal::service("database"); //完全等同於\Drupal::database();  
    $con=$container()->get("database"); //效果同上,在容器物件可用時使用  
    $con=\Drupal::service("database.replica"); //獲取配置中$databases['default']['replica']表示的備用資料庫連結,無設定將回退到主庫  
    $con=$container()->get("database.replica"); //效果同上,在容器物件可用時使用  

以上方法返回一個連線物件,她封裝了常用的資料庫操作介面,其中:
$con->select($table, $alias = NULL, array $options = [])

返回一個查詢物件,本主題要講的實體查詢使用該物件執行資料庫查詢

資料庫元件提供的查詢物件:
用於構造一條sql查詢語句,她提供許多方法讓我們可以操作語句中的不同部分,可以構造任意的select語句,在最終執行時她可以根據各方法提供的資料自動組織產生一條sql語句,然後使用前文提到的連線物件的query方法進行查詢,查詢物件見:
\Drupal\Core\Database\Query\Select(mysql預設完全使用該類,其他資料庫可能用子類解決方言問題)
查詢後返回一個Statement物件供我們操作查詢結果資料,該Statement物件見:
\Drupal\Core\Database\Statement
在查詢前系統派發鉤子,傳遞該查詢物件給模組,以便各模組有機會修改她或者執行一些操作。為了觸發特定的鉤子和傳遞額外資料,查詢物件實現了以下介面:
\Drupal\Core\Database\Query\AlterableInterface
她使用了查詢標籤和查詢源資料,見下。

查詢標籤Tags:

用於標識一條查詢(查詢物件),從而執行特定的模組鉤子(實際上標籤決定了派發的鉤子名,見下文),一個查詢可以有任意數量的查詢標籤,可以用查詢物件的addTag($tag)方法新增,查詢標籤命名規則為:字母、數字及下劃線組成,全部小寫並以字母開頭

查詢源資料MetaData:
執行查詢相關的鉤子函式時,僅傳遞了查詢物件,但查詢物件可以攜帶任意數量的額外資料給模組鉤子,通過查詢物件的addMetaData($key, $object)方法新增, $object是附加傳遞的變數,可以是任意php變數,$key是一個字串值,是被傳遞變數的識別標誌,命名規則和php的變數命名規則相同,模組鉤子通過它取回附加變數

查詢修改鉤子:
在查詢物件執行真正的資料庫查詢前會執行各模組的查詢修改鉤子:
\Drupal::moduleHandler()->alter($hooks, $query);
$query為查詢物件,$hooks為一個修改鉤子構成的陣列,各修改鉤子的名字組成為:'query_' . $tag,各模組如果需要執行和查詢物件有關的邏輯可以針對其不同標籤進行鉤子實現,如果針對所有查詢進行鉤子實現,那麼鉤子名為'query',具體鉤子的函式名如下:
假設標籤為node_access,模組名為yunke,那麼鉤子函式名為:yunke_query_node_access_alter
針對所有查詢的鉤子名為:yunke_query_alter

在回顧完必要的資料庫元件知識後,來看一看實體查詢,這之前提醒你回顧並充分理解SQL的join用法,這是實體查詢的核心知識點,從程式碼級別看實體查詢的目的就是要構建出聯結多個表並設定條件的查詢語句

實體查詢entityQuery:
實體查詢使用一個實體查詢物件去執行查詢相關功能,實體查詢和實體儲存息息相關,後者為前者提供儲存結構,因此實體查詢才知道如何構建SQL語句,實體查詢物件是從實體儲存處理器中返回的:

$entityTypeManager->getStorage($entity_type)->getQuery($conjunction);
$entityTypeManager->getStorage($entity_type)->getAggregateQuery($conjunction);
系統提供了以下快捷獲取方法:
\Drupal::entityQuery($entity_type, $conjunction = 'AND');
\Drupal::entityQueryAggregate($entity_type, $conjunction = 'AND');
各實體型別在實體儲存處理器的getQueryServiceName方法中返回自己型別需要的實體查詢工廠服務(容器服務id),由該工廠服務產生實體查詢物件,這裡以節點實體為列,她的查詢工廠服務如下:
服務id:entity.query.sql
類:Drupal\Core\Entity\Query\Sql\QueryFactory
該工廠服務不僅為節點實體所用,還可以為各種實體型別產生用於sql的實體查詢物件,她返回的查詢物件是系統提供的預設實體查詢服務,見下,我們應該總是通過實體查詢工廠去例項化實體查詢物件,而不是直接例項化。

預設常規實體查詢物件:
類:\Drupal\Core\Entity\Query\Sql\Query
實現介面:\Drupal\Core\Entity\Query\QueryInterface
所有的實體查詢都應該實現該介面,裡面描述了實體查詢物件提供的功能方法,要知道怎麼使用實體查詢看該方法即可,該介面繼承了以下介面:
\Drupal\Core\Database\Query\AlterableInterface
這就為鉤子派發打下了基礎,實體查詢物件內部包裝著資料庫查詢物件。
實體查詢的所有方法你可以檢視前文提到的介面文件,這裡我們重點講解以下方法的使用:
$entity_query->condition($property, $value = NULL, $operator = NULL, $langcode = NULL)
該方法中$property為條件屬性名,也就是要針對其進行條件檢查的欄位物件屬性名,她也可以是另外一個條件物件(見下文的條件組)。
條件屬性名:
條件中使用的屬性名採用欄位物件及其屬性名,而不是資料庫中的表列名(使用時我們不需要知道那麼細節的事情,實體是高階抽象,不需要關注儲存細節),可以是如下幾種:
欄位名+“.”+屬性名:
屬性名是定義在欄位物件的schema方法裡面的列名字,而不是資料庫列名,資料庫列名可能是在屬性名前加了字首的
僅僅一個欄位名:
如果是基本欄位(也就是儲存在共享表裡面的欄位,目前僅為單屬性欄位)或屬性名為主屬性名,那麼屬性名可以省略,因此可以簡寫為僅一個欄位名
欄位名+“.”+“下標”+“.”+屬性名:
可用下標控制搜尋多值欄位(注意多值和多屬性的差別),下標可以是一個具體的數字,表示只搜尋該下標值中屬性名滿足條件的實體,注意在多值欄位中第一個值下標為0;下標也可以用“%delta”指代任意下標值,這種情況等效於欄位名加屬性名,因此可以省略
欄位名+“.”+“下標”
如果下標為數字,等效於欄位名+“.”+“下標”+“欄位主屬性名”
如果下標為“%delta”,那麼條件操作的是下標屬性值,相當於以上第一種情況中屬性名等於“delta”,但是不能寫為fieldname.delta,因為delta是一個特殊的屬性,並未在欄位定義中指出,僅存在於資料庫實現層面
欄位名+“.”+“引用型別”+“:實體型別id”+“.”+欄位名…
這用於引用型別欄位的搜尋,功能強大,可進行跨實體型別搜尋,如在節點實體中uid.entity:user.name表示uid儲存的的使用者的使用者名稱,這關聯到了使用者實體;這種模式下首先欄位名代表的必須是一個引用型別的欄位,“引用型別”通常是“entity”,表示引用到另外一個實體,此識別符號被定義在:
\Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem的propertyDefinitions方法中
“:實體型別id”部分指示引用到的實體型別,可以省略,因此前例等同於uid.entity.name,從減少出錯的角度看雲客建議省略,這樣系統會根據欄位定義自動計算引用到的實體型別,從效能角度看可以不省略,但也快不了多少,該種模式下末尾的“欄位名…”就是新實體型別上的欄位名了,且可以重複該模式以進行更加複雜的搜尋。

條件值:
條件操作的值通常是一個標量值,或者陣列,陣列中的元素含義依賴於操作符;如果值是字串則是否大小寫敏感取決於欄位屬性定義中的case_sensitive項設定:
$fieldStorageDefinition->getPropertyDefinitions()->getSetting('case_sensitive');

條件操作符:
實體查詢的條件操作符可以使用資料庫元件提供的所有操作符,在此基礎上為了方便使用進一步的封裝了一些高階操作符號,如下:
'<>':不等於,大小寫不敏感時內部被轉化為NOT LIKE
'STARTS_WITH':以什麼開始,通常用於字串搜尋,內部被轉化為LIKE value%
'CONTAINS':包含什麼,內部被轉化為LIKE %value%
'ENDS_WITH':以什麼結束,內部被轉化為LIKE %value
當搜尋的值大小寫敏感時,LIKE部分使用LIKE BINARY
注意條件操作符本身是大小寫不敏感的,如“in”也可以是“IN、In、iN”,在執行語句上會被統一為大寫

語言程式碼:
指示搜尋被限定在某一種語言上,如果是所有語言,那麼省略該引數,這作用在JOIN的ON條件上,也就是先通過語言程式碼過濾資料再進行聯結

查詢舉例:
舉一個列子來說明,這裡假設將節點實體的body欄位設定為多值欄位,執行下面的程式碼:

$entity_query=\Drupal::entityQuery("node", 'AND');
$entity_query->condition("title", [520], 'IN');
//查詢標題為“520”的節點
$entity_query->condition("body.2.summary", "yunke", '=');
//查詢body欄位下標為2的值(第三個值)中摘要為“yunke”的節點
$entity_query->condition("body.%delta.summary", "yunke", '=');
//查詢body欄位任意下標中摘要為“yunke”的節點,這其實包含body.2.summary,也等效body.summary,所以我們在進行實體查詢時需要注意優化
$entity_query->condition("body.3", 5, 'IN');
//查詢body下標為3的值中主屬性等於5的節點,body的主屬性為“value”,等效於body.3.value
$entity_query->condition("body.%delta", 5, 'IN');
//查詢body欄位下標值為5的節點,條件作用在列名delta上
$entity_query->allRevisions();
//對全部版本進行查詢
$ids=$entity_query->execute();
產生的查詢語句如下:
SELECT base_table.vid AS vid, base_table.nid AS nid
FROM 
{node_revision} base_table
INNER JOIN {node_field_revision} node_field_revision 
ON node_field_revision.vid = base_table.vid
INNER JOIN {node_revision__body} node_revision__body 
ON node_revision__body.revision_id = base_table.vid AND node_revision__body.delta = :delta0
INNER JOIN {node_revision__body} node_revision__body_2 
ON node_revision__body_2.revision_id = base_table.vid
INNER JOIN {node_revision__body} node_revision__body_3 
ON node_revision__body_3.revision_id = base_table.vid AND node_revision__body_3.delta = :delta1
WHERE 
(node_field_revision.title IN (:db_condition_placeholder_2)) 
AND (node_revision__body.body_summary = :db_condition_placeholder_3) 
AND (node_revision__body_2.body_summary = :db_condition_placeholder_4) 
AND (node_revision__body_3.body_value IN (:db_condition_placeholder_5)) 
AND (node_revision__body_2.delta IN (:db_condition_placeholder_6))
再來看一下引用型別欄位的搜尋:
$entity_query=\Drupal::entityQuery("node", 'or');
$entity_query->condition("uid.entity.name", "yunke", '=');
//找出作者為“yunke”的所有節點實體
//等同於:$entity_query->condition("uid.entity:user.name", "yunke", '=');
$ids=$entity_query->execute();
產生的查詢語句如下:
SELECT base_table.vid AS vid, base_table.nid AS nid
FROM 
{node} base_table
LEFT JOIN {node_field_data} node_field_data ON node_field_data.nid = base_table.nid
LEFT OUTER JOIN {users} users ON users.uid = node_field_data.uid
LEFT JOIN {users_field_data} users_field_data ON users_field_data.uid = users.uid
WHERE users_field_data.name LIKE :db_condition_placeholder_0 ESCAPE '\\'

查詢返回值:
如果是計數查詢(呼叫了count())則返回一個整數,代表符合條件的實體總數,如果是非計數查詢返回一個數組,陣列的鍵名是版本id,鍵值為實體id,如下:
        $entity_query=\Drupal::entityQuery("node", 'or');
        $entity_query->allRevisions();
        $entity_query->count();
        $arr=$entity_query->execute();
        print_r($arr);
        exit();
以上程式碼輸出一個整數,如果沒有呼叫$entity_query->count();那麼輸出類似如下:
Array
(
    [36] => 9
    [47] => 9
    [48] => 9
    [37] => 10
    [38] => 11
    [39] => 12
    [40] => 13
    [41] => 14
    [42] => 15
    [43] => 16
)
有了實體id或版本id就可以使用實體儲存處理器一節講的方法對實體進行操作了

查詢條件組:
多個條件通過連線詞構成一個條件組,在同一個條件組中連線詞是一樣的,要麼為AND,要麼為OR,但我們可以將多個條件組再次用連線詞連線構成一個更大的條件組,條件組的巢狀可以是多層的,這樣我們就可以實現“且”和“或”的混用,關於這一點在補充說明中有更進一步的解釋,實體查詢也可以使用條件組,比如要進行這樣的查詢:
找出作者為“yunke”或“admin”的使用者釋出的文章,且釋出狀態為釋出
可以使用以下程式碼進行:
        $query = \Drupal::entityQuery('node', "AND");
        $group = $query->orConditionGroup()
            ->condition('uid.entity:user.name', 'admin')
            ->condition('uid.entity:user.name', 'yunke');
        $query->condition($group);
        $group = $query->andConditionGroup()
            ->condition('status', '1')
            ->condition('type', 'article');
        $query->condition($group);
        $entity_ids = $query->execute();
查詢優化:
在實體查詢中,每新增一個條件,在最終執行的SQL語句都會新增一個聯結語句,哪怕是同一個表,也會用不同的別名進行多次聯結,這在資料庫中需要進行復雜的詞法優化,在有些資料庫上甚至對結聯次數有限制,所以在設計條件語句的時候需要注意該問題,儘量減少聯結次數

預設實體聚集查詢物件:
類:\Drupal\Core\Entity\Query\Sql\QueryAggregate
實現介面:\Drupal\Core\Entity\Query\QueryAggregateInterface
聚集查詢是對某欄位屬性執行這些SQL函式SUM、 AVG、 MIN、 MAX、COUNT,她繼承自基本實體查詢,你需要先充分理解SQL的聚集函式、分組查詢GROUP BY、分組條件HAVING等等
要知道她的使用,用一個列子是直觀的:
假設在我們的系統中有兩個內容型別鋼筆“pen”和腕錶“watches”,他們共用一個價格欄位“price”,現在要找出最便宜的鋼筆和腕錶的價格,可以用如下程式碼:
        $entity_query = \Drupal::entityQueryAggregate("node");
        $entity_query->aggregate("field_price", "MIN");
        $entity_query->groupBy("type");
        $entity_query->condition("type", ["pen","watches"], 'IN');
        $arr = $entity_query->execute();
        print_r($arr);
        exit();
輸出:
Array
(
    [0] => Array
        (
            [type] => pen
            [field_price_min] => 1038
        )

    [1] => Array
        (
            [type] => watches
            [field_price_min] => 260
        )

)
執行的sql語句為:
SELECT node_field_data_2.type AS type, min(node__field_price.field_price_value) AS field_price_min
FROM 
{node} base_table
LEFT JOIN {node__field_price} node__field_price ON node__field_price.entity_id = base_table.nid
INNER JOIN {node_field_data} node_field_data ON node_field_data.nid = base_table.nid
LEFT JOIN {node_field_data} node_field_data_2 ON node_field_data_2.nid = base_table.nid
WHERE node_field_data.type IN (:db_condition_placeholder_0, :db_condition_placeholder_1)
GROUP BY node_field_data_2.type
更多方法的使用請看介面中的註釋


自定義實體查詢:
在預設的查詢工廠(服務id:entity.query.sql)中可以看到我們可以如何自定義自己的查詢物件:
1、定義一個查詢工廠服務,繼承系統預設工廠服務
2、在儲存處理器的getQueryServiceName方法中指定第一步定義的服務
3、在自定義的工廠服務同級目錄定義查詢物件(類名:Query)及聚合查詢物件(QueryAggregate)即可(檔名與類名相同,和自定義的工廠服務在相同名字空間下)
自定義的查詢服務往往可以繼承系統預設提供的查詢服務:
\Drupal\Core\Entity\Query\Sql\Query
\Drupal\Core\Entity\Query\Sql\QueryAggregate
並在此基礎上進行需要的修改

實體查詢在儲存處理器中的簡單使用:
看完實體查詢後現在你應該能很容易的理解儲存處理器中的以下方法了:
根據實體的屬性值載入實體:
public function loadByProperties(array $values = []);
public function getQuery($conjunction = 'AND');
public function getAggregateQuery($conjunction = 'AND');

補充說明:
1、在技術類書籍中以雲客的經驗來看側重從事開發工作的作者比從事教育工作的作者講的更加透徹,不會紙上談兵忽略關鍵點,可能出現的疑點往往也能主動提出並講的很清楚,這得益於開發工作者有真實的開發經歷,你想到的或會遇到的往往他們已經想到遇到過了,這裡給大家推薦兩部關於資料庫的書籍:《SQL必知必會》作者Ben Forta,讀完這本書你學習drupal足夠了,如果想成為資料庫專家可以繼續讀《高效能Mysql》作者Baron Schwartz等著。

2、你可能會疑惑在drupal的資料庫實現中,查詢條件物件只使用一種連線詞去連線所有的條件,換句話說在查詢條件巢狀層級中每一層使用相同連結詞(要麼and要麼or,該連線詞在條件物件初始化時傳入),同一層不會進行連線詞混用,不能and和or混用嗎?這是為什麼呢?原因在於sql語句中AND的優先順序高於OR,where 後面同時有and和or條件,同一層混用時,是先用or將語句分隔成幾部分,這相當於為and部分加上括號再用or連線,本質上就是巢狀層級中每一層使用了相同連線詞,所以drupal的實現並不妨礙查詢執行,如果需要and和or的組合查詢,總是能用條件組去實現,只需要將他們放入不同層級中。

3、實體查詢是跨bundle的,是針對整個實體型別進行查詢

4、在資料庫元件的查詢條件中操作符沒有“!=”和“<>” 使用NOT IN代替,但在實體查詢中有“<>”

5、使用LIKE時,如果是未知值,需要先轉義$sql_query->escapeLike($condition['value'])

6、完整SELECT查詢結構及各部分順序:

SELECT  DISTINCT(可選DISTINCT) 
(TablesAlias.field 或expression  AS  FieldAlias)多個用逗號分隔
FROM  Tables或(子查詢)  TablesAlias
INNER或LEFT OUTER或RIGHT OUTER   JOIN  Tables或(子查詢)  TablesAlias   ON  條件
前條可多個用空格分隔或換行
WHERE  條件
GROUP BY  分組欄位 
HAVING  條件
UNION  ALL(可選ALL) (子查詢)
前條可多個用空格分隔或換行
ORDER BY  (欄位  DESC降序或ASC升序)可多個用逗號分隔
LIMIT  length  OFFSET  start
FOR UPDATE

7、bug:欄位的儲存屬性設定會聯動到所有bundle,比如在文章bundle中設定body欄位的允許數量為不限,那麼其他所有bundle都會被設定為不限,這是因為儲存設定是比bundle更加底層的概念,一個實體型別中同一個欄位共享相同儲存設定,而不管該欄位在哪個bundle中,多個bundle可以共用這個欄位;這在有些情況下是不合理的,但沒有關係,如果確實需要,我們可以新建相同型別的欄位以滿足需求


作者語:
該篇主題作為2017年的最後一篇,花了不少時間,完成時已是2017年12月29日晚上8點,元旦即將到來,翻年在即,此刻雲客心情五味雜陳,這一年裡將本應該陪伴在兩個孩子身邊的時間用在了drupal寫作上面,錯過了太多有趣溫馨的畫面,很快他們將長大,心裡有許多內疚和遺憾升起,希望這一切都值得,希望在未來的某個時間點為drupal在中國的普及會心一笑,作為一名技術人員很喜歡一句話“我願做一名燈塔下孤獨的敲鐘人”,這好像是愛因斯坦說的,孤獨才能靜心,靜心才能看到技術之美,才能走遠,在來年期待遇到更多在drupal這條路上走遠的人。


我是雲客,【雲遊天下,做客四方】,微訊號:php-world,歡迎轉載,但須註明出處,討論請加qq群203286137