提供EAV(实体-属性-值)设计模式。

安装:1,563

依赖项: 2

建议者: 0

安全: 0

星标: 5

关注者: 3

分支: 4

类型:cakephp-plugin

dev-master 2018-05-11 23:15 UTC

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_ageUser-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定义了两个不同的列,articlepage,现在我们可以找到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属性仅存在于articlebundle中,您将得到之前描述的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);