49. 实体Entity(四):内容实体概述Content Entity
内容实体用于处理系统四大信息类别(内容、会话、状态、配置)中最主要的内容信息,如栏目内容、评论内容、会员数据、文件信息等等,是一个比较庞大的子系统,涉及内容很多,本主题介绍一个概况,帮助后续学习,在后续主题中对各细节会有非常详细的介绍。
数据建模:
如果你是第一次听到这个词,可能会觉得抽象,但它的含义却是很简单的,就是为一个对象确定数据模型的过程,这里举一个列子来说明:假设你要做一个卖杯子的网站,那么你必须先思考这个网站会涉及到你所卖杯子的哪些属性,比如品牌、型号、价格、材质、容积、是否保温、库存等等,哪些属性是必须的呢?哪些属性会出现相关依赖属性呢(比如杯子是保温的,那么就会出现保温时长属性)?每个属性的值怎么表示呢?可以通过哪些属性进行检索或筛选呢?应该怎么命名每个属性呢?
回答这些问题的过程就是在进行数据建模,完成以后一个数据模型就出现了,接下来你要考虑数据的储存问题,可以进一步的将通用的属性归到一大类,称作基本数据模型,将不通用的属性归为另外一大类,称为扩展数据模型,在drupal中用“内容类型”这个名字来指代数据模型,在有些cms中有不同的称呼,比如帝国cms中称为“系统模型”。
在研究cms系统管理的信息时,我们会发现有一类信息会具备一些共同属性,比如新闻、产品、评论等等它们都具备:建立时间、发布状态、索引ID、作者等等,会发现这一类的信息都是内容,是cms管理的主要对象,这在drupal中称为内容实体,而它们具备的那些通用属性形成了所有内容信息的基本数据模型,相对应的,由于基本数据模型的共有性产生了节点这个概念,节点可以检索所有这些具备共有性的内容,并且系统预设了这些属性,面对具体的某一类内容我们只需要定义扩展数据模型就可以了,我们定义的实际上是扩展数据模型,称为bundles,见下文,在不同的bundle中对共同属性的命名可能是不一样的,比如共同属性:ID在产品模型中可能是型号,在新闻中可能是标题,由此又产生了标准属性名这个概念,在drupal中称为实体标准键名,见下。
Bundles
官方解释为一种用于信息的容器类型,有时被称为子类型,这解释听起来有点抽象,直白一点说就是:扩展数据模型。举个列子:比如我们要建立一个关于汽车的网站,首先需要对汽车进行数据建模以储存汽车的属性,比如品牌、价格、座位数等等,这些都是所有汽车通用的属性,这些属性一起形成了一个通用(基本)汽车数据模型,但有些属性却不是通用的,比如货箱大小,这只有货车才具备,因此把货车特有的属性归纳到一个扩展的货车数据模型中,该扩展模型就是前面讲到的通用汽车数据模型的bundle,货车数据模型和通用汽车数据模型一起就能描述货车的所有属性了,在drupal中节点和内容类型和汽车的列子是一样的,节点储存的信息都是通用的,bundle储存描述具体的一种信息,换个角度看bundle就是节点的子类型,节点只代表一个通用信息,bundle代表一个具体的信息;在继承中子类型比父类型有更多属性需要描述,明白这些才能理解后续的基本字段和bundle字段是什么意思了(bundle字段是子类型相较父类型多出来的、特有的字段);
bundle是可选的,目前(drupal8.3.x)所有的配置实体暂无bundle,内容实体部分有bundle,从bundle的作用来看,理论上讲可以有多级bundle,也就是bundle还可以有bundle,依次扩展,但目前drupal系统只实现了一级,这和目前网站应用的复杂度有关系,多级有待将来实现。
以上讲的都在站在系统层面来看的,对于用户来说,建立一个内容类型感觉上好像自己在建立一个基本数据模型,但在系统层面来看实际是建立了一个bundle。将来bundle这个概念可能会上升到使用层面,那时可能成为内容类型的子类型(扩展类型),因为bundle是可以多级的,但实现起来复杂度、性能等会增加,有无必要还不好说。
实体标准键名:
实体是一个抽象级别较高的概念,高度抽象就会有统一概念,这和细节实现会有些许差异,比如实体id用“id”来表示,用户实体的id却用“uid”来表示,为了屏蔽这些差异,便于用统一的方式和实体交互,我们需要将一些属性标准化,这就是实体释文定义中entity_keys键的作用,它是一个数组,键名就是实体标准键名,键值为具体名,如上所示的用户实体中“id”就是实体标准键名,而“uid”则为标准键名对应的具体名,这往往是字段名,在本系列主题中以后会多次用到“实体标准键名”这个术语,有时候也指“实体标准属性”。
内容实体和配置实体间的区别:
1、判断是否为新建的方式不同,内容实体如果没有实体id即被认为是新建的,而配置实体生来就有实体id,后者靠人为设置来判断
2、配置实体建立在配置系统之上,在数据库中整个实体数据作为一个数组储存在配置表的一个字段里,内容实体则不同,她的储存是建立在字段系统之上,每个属性均储存在一个单独的字段中
3、翻译机制不同,配置实体使用配置覆写来处理翻译,依赖于翻译系统,而内容实体直接储存各语言的翻译版本,并不依赖翻译系统
4、在drupal 8.3.x及以前版本中暂不支持配置实体的类型化,未来待定
5、配置实体使用Schema 文件,内容实体使用hook_schema()
以上只列出了主要区别,更多请见后续主题
数据库储存:
在drupal中内容实体的储存是建立在字段组件之上的,将在本系列后续主题中介绍;我们谈及字段组件中的字段和数据库中的字段时含义是有区别的,前者含义更宏观(是一个包含多个信息的数据对象),除了包含了后者外,还有许多与后者相关的属性和配置,前者直接充当实体的属性;在数据库字段这个层面上,许多cms将同一个数据模型的多个相关属性一一对应一个字段并储存在数据库的同一张表里,而drupal则不同,她为每一个属性建立一张表,表中有一个值字段来储存该属性的值,并提供其他字段来储存与该属性相关的信息,整张数据表向上层应用抽象成一个“字段”,最终形成字段数据对象;所以他们的含义是不一样的,但在使用层面感觉不到这一点,drupal的实现更加灵活,细粒度更低,带来很多很棒的功能,比如:系统允许多数据模型(内容类型)共用一个“字段”,这在底层实际上是一张字段表储存了不同内容类型的信息,在表中用bundle字段加以区别;自然实现了多值字段功能,支持翻译,十分优雅。
版本:
内容实体可以设置版本,这是一个很棒的功能,可以保存以往的修改历史记录,还可以进行回退,相当于一个简易的GIT
翻译:
通常来说我们认为的翻译就是将一种语言转换为另外一种语言,但这是狭义上的,带着这种狭义的理解去研究Drupal内容的多语言机制会遇到障碍,广义的翻译有两个关键词:信息与表达形式,翻译是将同一个信息在不同表达形式之间进行转换,这里的信息和信息论中的信息是相同的(见香农信息论),信息不是表达形式,但在现实生活中人们常常并不在意其区别,一个信息的表达形式可以是文字、图像、声音,有时候还可能是一个动作,那么翻译可以是这些事情:
将中文的文字转变为英语的声音
将一张图片在不同格式之间转换
通过编译原理将Java程序转换成C++程序
将文字转化为手语
将一张图片转换为电磁波
可以看出只要信息具备多个表达形式就具备可翻译性,这种可翻译性是相对于广义翻译概念而言的,而在Drupal系统中翻译时常指狭义上的一种语言文字到另一种语言文字的转换;人类文字有共识的地方,这种共识性仅限于人类,比如数字,阿拉伯数字现今几乎可以用于所有人类语言的书面文字中,使用不同文字的人们都能看懂,因此这样的内容是不需要翻译的,所以在drupal系统中会看到不可翻译性这个说法,就是指代这样的内容,除数字外还有ID、标志符等等,相对狭义翻译而言广义的翻译则称为“转换”更加合适;在系统实现层面依然以广义翻译概念作背景为所有信息的表达形式指定了标识符,这就是语言代码,在广义翻译理解中语言代码就是一种表达形式的ID,在系统中狭义理解的“不可翻译”内容也是有语言代码的,用于指代一种人类可明白的表达形式,该语言代码用以下常量表示:
Drupal\Core\Language\LanguageInterface::LANGCODE_DEFAULT
该常量值为:'x-default',我们常见的语言代码有:简体中文zh-hans 、繁体中文zh-hant 、英文en,关于语言代码请见本系列语言主题。
语言代码是翻译的基础,在一个可翻译的内容实体第一次建立时必有一种对应的语言,该语言是该实体的源语言,这个实体的所有翻译均是基于此源语言的,在程序中有时候需要引用到源语言,但尚不知道源语言是什么,此时可以用以上提到的常量'x-default'表示,前文提到该常量表示“不可翻译”的内容的语言代码,为什么源语言可以用它表示呢?这是因为源语言相对于它自己是不可再被翻译的,只能翻译为其他语言,所以该常量有时充当源语言的别名,“源语言”这个概念很重要,以后将多次用到。
需要注意真正的翻译工作还是要由人工进行,drupal并不具备智能翻译功能,只提供形式上的区分,这属于两个层面上的事情;也不能根据录入的信息自动判断出是哪一种语言,如果录入的是简体中文,但在语言选择器中选的却是繁体中文,那么drupal就认为你录入的是繁体中文,以后也一直将录入的字符串当做繁体中文看待,尽管真实情况下那是简体中文。
版本和翻译间的储存逻辑:
一个节点建立后,会形成该节点的第一个版本,建立时采用的语言是该节点的源语言,所有翻译均是基于该语言,在添加翻译的编辑界面中,内容框显示的是源语言中的内容,如果添加翻译前源语言已经新建过几个版本,那么采用最新的版本,一旦执行了添加翻译,在以后新建版本后执行本语言的翻译更新时,编辑框使用本语言前一个翻译版本的内容,而非源语言的最新版本;如果字段被设置为是不可翻译的,那么在任意一种语言下进行了修改,则所有翻译均会被修改;
在任何一种语言下均可新建版本,新建版本时所有的已添加翻译的语言都会在数据库中建立相同版本id(vid)的新版本,新建版本时选择的语言以外的其他语言的新版本是直接复制该语言的前一个版本内容,并不是新建版本时的语言内容,在这些语言的新版本中等待人工更正翻译为新内容,此时在该语言的版本选项卡中只列出以往版本新建后经过翻译更新的版本;
在任一语言中恢复到某个版本的过程和建立一个新版本是一样的,并不是所有操作回退,而是所有语言继续新建一个版本(注意是所有语言),在恢复操作时处于的语言下新建的版本从要恢复的版本复制内容,但这种复制操作只限于该语言,而其他语言依然是复制新增版本时前一个版本,也就是说在某语言下恢复到一个较久的版本时,只是该语言恢复了,其他语言保持不变,在数据库中实际上是全部翻译语言都增加了一个版本,复制了一份数据且vid增加了,在这个恢复的过程中不会删除任何数据。
理解这个处理流程是理解内容实体储存必不可少的前提。
这样的处理机制可能会造成数据库出现大量重复数据,比如当一种语言连续创建多个版本,而期间其它语言已添加翻译但新建版本后没有发生及时的人工翻译更新,在这种情况下在数据库中每一次创建版本时其它语言都会将自己对应的前一个版本复制一次,从而产生多个相同副本,而这些相同副本只有最新的才被引用到,其他副本成为无用的僵尸数据,占用着数据库空间,为了避免这个问题,使用者最好明白创建新版本意味着什么,仅原版本需要存档时才创建新版本,而不是每一次修改都创建,否则开销是很大的,实际使用中每创建一个新版本,人工翻译工作要及时进行;drupal的这种实现机制云客认为是需要改进的,太过暴力,当创建某语言的新版本时,在数据库中其他语言不应该立即复制数据,而应该在新版本创建后有翻译更新时才建立版本数据,此外在恢复过程中需要提供一个选项指示是否将其他语言一并恢复,这有待未来改进。
我是云客,【云游天下,做客四方】,微信号:PHP-world,欢迎转载,但须注明出处,讨论请加qq群203286137