quickapps-plugins / eav
提供EAV(实体-属性-值)设计模式。
Suggests
- quickapps-plugins/cms: Allows to store extended meta-information for each virtual column as a serialized array.
This package is not auto-updated.
Last update: 2024-09-14 17:42:47 UTC
README
实体-属性-值模型(EAV)是一种用于描述实体的数据模型,其中可以用于描述这些实体的属性(属性、参数)数量可能非常大,但实际上适用于特定实体的属性数量相对较少。在数学中,此模型被称为稀疏矩阵。EAV还被称为对象-属性-值模型、垂直数据库模型和开放模式。
——维基百科
简介
当您有具有可变数量属性的实体,并且这些属性可以具有不同类型时,您通常会使用EAV模式。这使得无法将这些属性定义为实体的表中的列,因为会有太多列,其中大多数将没有数据,并且由于关系数据库中的列需要预先定义,您根本无法处理动态属性。
为了以关系式解决这个问题,您会创建一个子表,并使用一对一关系将其与“实体”表相关联,其中每个属性都将成为子表中的一个记录。然而,这种方法的一个缺点是,为了能够获取特定的属性值,您将不得不遍历所有相关记录,将属性列的值与您正在查找的属性进行比较,如果找到匹配项,则获取值列的内容。
EAV插件使用相同的实现方式,但允许您将虚拟属性与实体合并,从而使属性成为实体对象的属性。
安装
您可以使用Composer将EAV插件安装到您的CakePHP项目中
$ composer require quickapps-plugins/eav:"*"
插件加载
然后编辑您的项目bootstrap.php
文件,并确保EAV插件被正确加载
Plugin::load('Eav');
请查看Cake的文档以获取更多信息。
导入数据库模式
最后,使用提供的SQL脚本/config/eav-mysql.sql
并将其导入到您的项目数据库中(目前仅支持MySQL),这将创建两个由EAV插件内部使用来存储和定义虚拟属性的表。
使用
一旦EAV插件已加载到您的项目并且所有MySQL表都已创建,您就可以开始使用了。要开始使用EAV API,您必须将Eav.Eav
行为附加到您想要“扩展”的表(向其中添加虚拟列),例如
use Cake\ORM\Table; class UsersTable extends Table { public function initialize(Table $table) { $this->addBehavior('Eav.Eav'); } }
定义属性
一旦EAV行为附加到您的表,您现在可以开始定义虚拟列。定义虚拟列有两种方法:基于CLI或基于PHP脚本。我们将解释如何使用这两种方法定义此类列。
使用EAV CLI(推荐)
EAV插件提供了一个简单的管理命令行界面(CLI),允许您轻松地添加或删除虚拟列。
您需要指定正在更改的表、您希望执行的操作(添加新虚拟列或删除现有列)。如果您正在添加新列,则必须提供列信息(列名、数据类型等)。以下是一个添加名为user_age的新虚拟列的示例
user@name:/path/to/bin/$ cake Eav.table schema --use UsersPlugin.UsersTable --action add --name user_age --type integer --searchable
searchable
表示此虚拟列可以在WHERE
子句中使用。如果您想删除现有列
user@name:/path/to/bin/$ cake Eav.table schema --use UsersPlugin.UsersTable --action drop --name user_age
请查看EAV CLI的帮助以获取更多选项。
使用PHP脚本
警告
您只需执行此步骤一次,否则每次执行脚本时都会进行不必要的列更新。
您可以使用表的 addColumn()
方法创建新的虚拟列定义,此方法将 如果已存在则更新列信息。
use Cake\ORM\Table; class UsersTable extends Table { public function initialize(Table $table) { $this->addBehavior('Eav.Eav'); // WARNING: just run once these two lines $this->addColumn('user-age', ['type' => 'integer']); $this->addColumn('user-address', ['type' => 'string', 'bundle' => 'admin']); } }
第一个参数是您正在定义的列的名称,您 必须使用小写字母、数字或"-"符号。例如,user-age
是一个有效的列名,但 user_age
或 User-Age
则不是。
第二个参数用于定义列的元数据,并支持以下键
- type (字符串): 该属性的数据库类型,注意,使用此处未列出的任何其他类型将引发异常。支持的值有
- biginteger
- binary
- date
- float
- decimal
- integer
- time
- datetime
- timestamp
- uuid
- string
- text
- boolean
- bundle (字符串): 表示该属性属于表中的某个bundle名称,请参阅“Bundles”部分以获取更多信息。默认为 null(无bundle)。
- searchable (布尔值): 此属性是否可用于SQL的“WHERE”子句。默认为 true
删除虚拟列
您还可以使用 dropColumn()
方法删除之前使用 addColumn()
定义的现有虚拟列,为此,您可以使用 dropColumn()
方法
use Cake\ORM\Table; class UsersTable extends Table { public function initialize(Table $table) { $this->addBehavior('Eav.Eav'); $this->dropColumn('user-age'); $this->dropColumn('user-address', 'admin'); } }
可选地,第二个参数可以用于指示列所在的bundle。
警告
此方法将 删除与正在删除的列相关联的任何存储信息,因此请谨慎使用。
获取实体
在将行为附加到您的表并定义了一些虚拟列之后,您可以使用 "Table::find()" 或类似方式开始从您的表中获取实体,以这种方式获取的每个实体都将具有作为传统表列的附加属性。例如在任何控制器中
$user = $this->Users->get(1); debug($user) [ // ... 'properties' => [ 'id' => 1, // real table column 'name' => 'John', // real table column 'user-age' => 15 // EAV attribute 'user-phone' => '+34 256 896 200' // EAV attribute ] ]
您可以使用您的EAV属性如常;您可以对它们应用验证规则、在 WHERE 子句中使用它们、创建表单输入、保存实体等
$adults = $this->Users ->find() ->where(['Users.user-age >' => 18]) ->all();
注意
EAV API有一些限制,例如,您不能在ORDER BY子句、GROUP BY、HAVING或任何聚合函数中使用虚拟属性。
Bundle
Bundle是同一表内属性的子集。例如,我们可能有“文章页面”、“普通页面”等;它们都是Page实体,但它们可能具有不同的属性,这取决于它们属于哪个bundle。
$this->addColumn('article-body', ['type' => 'text', 'bundle' => 'article']); $this->addColumn('page-body', ['type' => 'text', 'bundle' => 'page']);
我们已经为两个不同的bundle定义了两个不同的列,article
和 page
,现在我们可以找到Page实体并仅获取某些bundle
的属性
$firstArticle = $this->Pages ->find('all', ['bundle' => 'article']) ->limit(1) ->first(); $firstPage = $this->Pages ->find('all', ['bundle' => 'page']) ->limit(1) ->first(); debug($firstArticle); // Produces: [ // ... 'properties' => [ 'id' => 1, 'article-body' => 'Lorem ipsum dolor sit amet ...', ] ] debug($firstPage); // Produces: [ // ... 'properties' => [ 'id' => 5, 'page-body' => 'Nulla consequat massa quis enim. Donec pede.', ] ]
在检索实体时如果没有给出bundle
选项,EAV行为将获取所有属性,无论它们属于哪个bundle。
$firstPage = $this->Pages ->find() ->limit(1) ->first(); debug($firstPage); // Produces: [ // ... 'properties' => [ 'id' => 5, 'article-body' => 'Lorem ipsum dolor sit amet ...', 'page-body' => null ] ]
警告
请注意,使用bundle
选项,您是在告诉EAV行为仅获取该bundle内的属性,这可能会导致使用不当时出现找不到列
SQL错误。
$this->Pages ->find('all', ['bundle' => 'page']) ->where(['article-body LIKE' => '%massa quis enim%']) ->limit(1) ->first();
由于article-body
属性仅存在于article
bundle中,您将得到之前描述的SQL错误。
EAV缓存
在某些情况下,当每次查询获取太多实体时,EAV可能会变慢,因为对于每个获取的实体,EAV插件都需要检索与该实体相关的所有虚拟列,也就是说,对于每个 实体集合,都会执行额外的SELECT
查询。为了改进这一点,EAV允许将每个实体的虚拟值作为序列化结构缓存到实体的实际列下。为此,您必须使用cache
选项指定EAV值将缓存的列名称,例如
将所有虚拟值缓存到eav_cache
列下
$this->addBehavior('Eav.Eav', ['cache' => 'eav_cache']);
在不同的列下缓存自定义的虚拟值集合
$this->addBehavior('Eav.Eav', [ 'cache' => [ 'contact_info' => ['user-name', 'user-address'], 'eav_all' => '*', ], ]);
访问缓存值
启用缓存后,您可以按以下方式访问缓存的EAV值
// controller use App\AppController; class UsersController extends AppController { public function index() { // load the model and fetch ALL USERS AT ONCE. $this->loadModel('Users'); $users = $this->Users->find('all', ['eav' => true]) $this->set('users', $users); } } // view foreach ($users as $user) { // physical column `name` $name = $user->get('name'); // virtual columns read from cache, read as follow: // $user->get(<cache_column_name>)->get(<virtual_column_name>); $age = $user->get('eav_cache')->get('user-age'); echo sprintf('%s is %s years old', $name, $age); }
限制
在每次实体更新后,缓存会自动更新。然而,在某些情况下,缓存可能会变得不同步。如果实体尚未更新/同步,您可能会看到之前已删除/修改的虚拟列的缓存值。
在更改虚拟列后更新每个实体的EAV缓存是一项非常昂贵的任务,这就是为什么EAV插件不会自动执行此任务。
总结一下,您需要注意以下情况
- 删除虚拟列后。
- 添加新的虚拟列后。
- 虚拟列的定义更改后(值类型等)。
注意
您可以使用表的updateEavCache()
方法更新单个实体的EAV缓存
$this->loadModel('Users'); $user = $this->Users->get($id), $this->Users->updateEavCache($entity);