60.实体查询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\Selectmysql默认完全使用该类,其他数据库可能用子类解决方言问题)

查询后返回一个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

 

在回顾完必要的数据库组件知识后,来看一看实体查询,这之前提醒你回顾并充分理解SQLjoin用法,这是实体查询的核心知识点,从代码级别看实体查询的目的就是要构建出联结多个表并设置条件的查询语句

 

实体查询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),由该工厂服务产生实体查询对象,这里以节点实体为列,她的查询工厂服务如下:

服务identity.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\EntityReferenceItempropertyDefinitions方法中

:实体类型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”也可以是“INIniN”,在执行语句上会被统一为大写

 

语言代码:

指示搜索被限定在某一种语言上,如果是所有语言,那么省略该参数,这作用在JOINON条件上,也就是先通过语言代码过滤数据再进行联结

 

查询举例:

举一个列子来说明,这里假设将节点实体的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 MAXCOUNT,她继承自基本实体查询,你需要先充分理解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

更多方法的使用请看接口中的注释

 

 

自定义实体查询:

在默认的查询工厂(服务identity.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,该连接词在条件对象初始化时传入),同一层不会进行连接词混用,不能andor混用吗?这是为什么呢?原因在于sql语句中AND的优先级高于ORwhere 后面同时有andor条件,同一层混用时,是先用or将语句分隔成几部分,这相当于为and部分加上括号再用or连接,本质上就是嵌套层级中每一层使用了相同连接词,所以drupal的实现并不妨碍查询执行,如果需要andor的组合查询,总是能用条件组去实现,只需要将他们放入不同层级中。

 

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

7bug:字段的储存属性设置会联动到所有bundle,比如在文章bundle中设置body字段的允许数量为不限,那么其他所有bundle都会被设置为不限,这是因为储存设置是比bundle更加底层的概念,一个实体类型中同一个字段共享相同储存设置,而不管该字段在哪个bundle中,多个bundle可以共用这个字段;这在有些情况下是不合理的,但没有关系,如果确实需要,我们可以新建相同类型的字段以满足需求

 

 

作者语:

该篇主题作为2017年的最后一篇,花了不少时间,完成时已是20171229日晚上8点,元旦即将到来,翻年在即,此刻云客心情五味杂陈,这一年里将本应该陪伴在两个孩子身边的时间用在了drupal写作上面,错过了太多有趣温馨的画面,很快他们将长大,心里有许多内疚和遗憾升起,希望这一切都值得,希望在未来的某个时间点为drupal在中国的普及会心一笑,作为一名技术人员很喜欢一句话“我愿做一名灯塔下孤独的敲钟人”,这好像是爱因斯坦说的,孤独才能静心,静心才能看到技术之美,才能走远,在来年期待遇到更多在drupal这条路上走远的人。

 

本书共94小节:

评论 (写第一个评论)