57. 内容实体数据库表结构及表映射table mapping
“字段”概念
在drupal中提到“字段”这个概念时,请不要理解为数据库中表的一个列,这不是一个概念,它是指一个字段对象,充当着实体对象的属性,也是一个列表类型的类型化数据对象,当本系列提到“字段field”一般均是指字段对象或字段定义对象,而数据库表中的字段列,则将其称为“列column”;一个字段对象可以包含数据库表中的多个列(取决于字段类型定义中的schema),这些列称为字段对象的属性,这样的字段叫做多属性字段;由于字段对象是列表类型的数据因此可包含数据库表中的多行数据,具体可以有多少行由字段定义对象中的基数参数决定,每行对应列表中的一个元素,当基数大于一或无限时,这样的字段称为多值字段(注意和多属性字段的区别)。关于字段对象的储存,可以多个字段储存在一张表中,也可以一个字段独立储存在一张属于她的专用表中,在一些特殊情况下一个字段对象还可以被储存在几张数据表中。
储存结构概述:
在一些cms系统中,是先设计数据库表结构,然后再围绕这个结构去开发程序,这很直观,但不够灵活,也不够强大,当需要额外的表时系统不能代为建立和感知,而drupal则不同,在drupal中内容管理主要是基于实体,实体又基于字段,系统是先找出实体所有的字段对象,再根据她们的储存定义自动计算出需要多少数据库表及每个字段应该放置在哪张表中,字段对象和她在数据库中的储存数据表的对应信息由表映射对象托管,而怎么决定这种映射关系,可以参看sql内容实体储存处理器中的getTableMapping方法(见下文),她是系统关于字段和储存表的默认映射规则。
表映射对象:
类:Drupal\Core\Entity\Sql\DefaultTableMapping
接口:\Drupal\Core\Entity\Sql\TableMappingInterface
该对象保存着实体字段对象到数据库表之间的映射信息,该类实例化后是一个没有数据的空对象,必须借助sql内容实体储存处理器为她注入数据,本质上该对象只是一个保存者,决定这种映射关系的逻辑代码实际位于sql内容实体储存处理器的getTableMapping方法中,我们明白了这个方法也就明白了内容实体的表结构。
默认内容实体数据库表结构:
为什么是默认呢?这是因为字段对象我们可以为其定义自定义储存,在默认情况下也是大多数情况下是用系统提供的储存机制,这里我们仅关注默认情况,要知道数据库表结构,需要先明白字段对象和数据库表的对应逻辑,核心代码位于:
Drupal\Core\Entity\Sql\SqlContentEntityStorage::getTableMapping(array $storage_definitions = NULL);
下文我们将详细分析该逻辑,这种对应逻辑适用于所有的内容实体,也就是说所有的内容实体类型的储存数据表遵循共有的规则(如表如何命名、哪些字段放哪个表、有哪些额外附加的列等),在系统中最主要的内容实体类型是节点实体,我们以它作为列子,然后你可以推及到其他内容实体,如用户user实体等。
要建立数据库表,需要知道内容实体的所有字段信息,这从实体字段管理器中获取(见本系列该主题),建表后再将这些字段分门别类的放到这些表中,一个内容实体类型的所有字段,系统使用五种类型的数据库表来储存,她们是:
基本表:
每个内容实体都必须存在的表,表名在实体类释文键“base_table”中指定,无指定则为实体类型id,比如节点实体类型表名为“node”
版本表:
仅在内容实体类型是可版本化时才存在,表名在实体释文键“revision_table”中指定,无指定则为实体类型id+'_revision',比如节点实体类型为“node_revision”
数据表:
仅在内容实体类型是可翻译的时才存在,表名在实体释文键“data_table”中指定,无指定则为实体类型id+'_field_data',比如节点实体类型为“node_field_data”
版本数据表:
仅在内容实体类型是可版本化且是可翻译的时才存在,该表存在时数据表必然存在,表名在实体释文键“revision_data_table”中指定,无指定则为实体类型id+'_field_revision',比如节点实体类型为“node_field_revision”
字段专用表:
当字段需要独立储存时为其建立的专用数据表,表名为:实体类型id+分隔符+字段名,字段专用表的分隔符为双下划线“__”,如果字段是可版本化的,那么还有字段版本专用表,命名规则一样,仅分隔符为“_revision__”,如果表名大于48个字符,那么做哈希处理,具体见上文表映射对象的generateFieldTableName方法,在系统管理后台:结构/内容类型管理中添加的字段,就是字段专用表来储存的,添加的字段名称被强制加上了“field_”前缀
注意以上提到的表名并不包括在数据库连接信息中指定的表前缀,如有指定需要加上才是数据库中真实的表名。
那么在一个内容实体中哪些字段存放在哪张表中呢?我们来详细分析上文提到的得到表映射方法:
Drupal\Core\Entity\Sql\SqlContentEntityStorage::getTableMapping(array $storage_definitions = NULL);
默认情况是从实体字段管理器(见本系列实体字段管理器主题)中获取一个内容实体类型所有的字段储存定义,然后首先找出可以和其他字段共同储存在一张数据表中的那些字段,她们被称为可共享储存字段,这些可共享储存表的字段必须同时满足三个规则:
1、非自定义储存,也就是使用系统默认储存
2、是基本字段,不能为bundle字段
3、不可多值储存,也就是一个字段对象只需要一行数据,不需要多行
该规则定义在以下方法中:
Drupal\Core\Entity\Sql\DefaultTableMapping::allowsSharedTableStorage
然后初始化以下变量并将可共享储存字段分类:
$key_fields:
关键字段,唯一标识一条信息的必要字段,是所有内容实体共有的字段,她们为:实体id、版本id、bundle、uuid、语言id,他们的字段名和存在性由实体类型决定
$all_fields:
全部可共享储存字段,包含关键字段且关键字段排序靠前
$revisionable_fields:
可共享储存字段中可版本化的字段
$revision_metadata_fields:
可版本化元数据字段(主要用于添加版本标注),字段名定义在实体释文的revision_metadata_keys键中
然后根据内容实体类型是否可版本化和可翻译性分四种情况,将可共享储存字段放入四种非专用表中,如下:
- 不可版本化且不可翻译
所有可共享储存字段$all_fields全部放在基本表中,此时不会有版本id和语言id字段,也没有其他共享表
- 可版本化但不可翻译
只建立基本表和版本表,全部可共享储存字段$all_fields排除版本元字段$revision_metadata_fields后放入基本表中,在版本表中放入可共享储存字段$all_fields中的可版本化字段,并加入版本关键字段:实体id和版本id
- 不可版本化但可翻译
只建立基本表和数据表,基本表中只储存关键字段$key_fields,全部可共享储存字段$all_fields放入数据表中(但uuid字段除外)。user实体类型即属于此情况,可查看数据库表:“users”为基本表,“users_field_data”为数据表
- 可版本化且可翻译
四种非专用表全部建立,节点实体即属于该种情况,基本表只储存关键字段$key_fields;可共享储存字段$all_fields排除uuid及版本化元字段$revision_metadata_fields后放入数据表中;实体id、版本id、语言id和版本化元字段一起放入版本表中;从可版本化字段$revisionable_fields中排除版本元字段及语言代码字段后和实体id、版本id、语言代码字段一起放入版本数据表中,这里排除语言代码字段又合并它是为了排序。
通过以上逻辑就将可共享储存表的所有字段划分到了四种共享储存数据表中,然后处理需要独立储存的字段,判断字段是否需要专用储存表的规则如下(同时满足):
- 非自定义储存,也就是使用系统默认储存的字段
- 可共享储存字段以外的全部字段
该规则定义在如下方法中:
Drupal\Core\Entity\Sql\DefaultTableMapping::requiresDedicatedTableStorage
字段储存专用表只有数据表和版本表两种,如果字段不是可版本化的则没有版本表,她们的表名按前文所述规则计算得出。
所有字段专用数据表都被附加了以下列:
['bundle','deleted','entity_id','revision_id','langcode','delta']
注意是“列”而不是字段,所有字段会被转化成为列然后和附加的列一起构成数据表的“列”。
字段对象转化为列的规则:
先需要明白字段的属性,她对应于字段储存定义对象的getColumns方法返回数组的键名,默认情况下在字段类型类的schema方法中定义。
在共享表中如果字段仅有一个属性,那么列名为字段名,注意此时不会理会字段的属性名,这将影响传递给实体类构造函数参数$values的结构,通常只有一个属性的字段属性名设置为“value”,如果多于一个属性名则列名为:$field_name . '__' . $property_name,注意是双下划线,在读取时系统根据列名是否有双下划线去判断该列是否为一个字段的属性。
在字段专用表中,如果字段属性为“%delta”那么列名为“delta”,如果属性名在系统保留列名(见补充)中,那么列名为属性名,其他情况则列名为$field_name . '_' . $property_name,这和共享表不一样,在字段只有一个属性的情况下并不以字段名命名,且注意是单下划线
以上字段转换成列的规则定义在以下方法中:
Drupal\Core\Entity\Sql\DefaultTableMapping::getColumnNames($field_name)
补充资料:
1、保留列名:用于系统功能,目前仅有“deleted”(表示实体是否已被删除),在字段类型设计中不能使用保留列名做字段列(字段属性)。
2、在后台录入信息时,默认情况下,录入信息的语言并不是根据当前界面的语言来自动识别的,而是语言选择器中输入,但该逻辑可以在管理内容类型的结构时在语言选项中指定,可以指定默认为界面语言
节点实体数据字典:
通过以上学习您应该明白了内容实体在数据库中的表结构,以下列出系统中最重要的节点内容实体的表结构,及其表中的列含义,俗称cms开发的“数据字典”(版本:drupal 8.4.0):
节点基本表(表名“node”),字段含义如下:
nid:节点id,或叫做节点内容实体的实体id
vid:版本id,在该表中储存节点当前版本(也是最新版本,版本的回退是在回退版本中复制数据形成新版本)
type:节点bundle名,也就是内容类型机器名
uuid:全局通用识别id,见本系列UUID主题,用于跨系统唯一标识一个信息对象
langcode :该节点该版本的源语言代码,未指定则为“und”,不适用则为“zxx”见本系列语言主题
节点版本表(表名“node_revision”),字段含义如下:
nid:同上
vid:该节点的各版本id
langcode:同上,该节点该版本的源语言代码
revision_timestamp:版本建立时的unix时间戳,非修改时间
revision_uid:进行版本建立操作的用户id
revision_log:版本注释(版本标识日志)
节点数据表(表名“node_field_data”),字段含义如下:
该表储存节点实体最新版本的所有翻译信息
nid:同上
vid:节点最新版本id
type:同上
langcode:该节点最新版本的各语言代码
status:布对外尔值,发表状态,1为已发表,0位未发表
title:节点标题,也是节点实体的label
uid:建立第一个版本时的用户id
created:该节点第一个版本建立时的时间戳,而不是当前版本的
changed:该节点最后一个版本的修改时间
promote:布尔值,是否推荐到首页,1为推荐
sticky:布尔值,是否置顶,1为置顶
revision_translation_affected:当该节点有多个翻译时,新建版本后,本语言是否已经进行了翻译更新,1为是
default_langcode:该节点的该版本的该语言,是否为源语言(一个版本的源语言只有一个,可修改)
content_translation_source:本翻译来自的源语言,如果本翻译就是源语言,那么值为“und”
content_translation_outdated:在内容翻译模块启用时才存在,布尔值,表示翻译是否已经过期,1为过期
节点版本数据表(表名“node_field_revision”),字段含义如下:
该表储存节点实体所有版本的所有翻译信息
nid:同上
vid:同上
langcode:该节点该版本的语言代码
status:同上
title:同上
uid:同上,注意在此表中,该字段表示第一个版本的创建用户,而不是指当前版本的创建用户
created:同上,注意是该节点第一个版本建立时的时间戳,而不是当前版本的建立时间
changed:同上
promote:同上
sticky:同上
revision_translation_affected:同上
default_langcode:同上
content_translation_source:同上
content_translation_outdated:同上
节点专用表列举,字段名“node__body”,字段含义如下(其他专用表类似):
bundle:通用附加列,储存bundle,也就是内容类型
deleted:通用附加列,表示内容是否已经被删除
entity_id:通用附加列,实体id
revision_id:通用附加列,实体版本id
langcode:通用附加列,语言代码
delta:通用附加列,当字段包含多值时,本条信息的数组下标,起到排序作用
body_value:字段body对应的value属性
body_summary:字段body对应的summary属性,表示摘要信息
body_format :字段body对应的format属性,表示字段允许的标签类型:受限制、基本的、完整的HTML