保存数据

class Cake\ORM\Table

在您加载数据后,您可能希望更新并保存更改。

保存数据的概览

应用程序通常有两种保存数据的方式。第一个显然是通过 Web 表单,另一个是直接在代码中生成或更改数据以发送到数据库。

插入数据

将数据插入数据库的最简单方法是创建一个新的实体,并将它传递给 Table 类中的 save() 方法。

use Cake\ORM\Locator\LocatorAwareTrait;

$articlesTable = $this->fetchTable('Articles');
$article = $articlesTable->newEmptyEntity();

$article->title = 'A New Article';
$article->body = 'This is the body of the article';

if ($articlesTable->save($article)) {
    // The $article entity contains the id now
    $id = $article->id;
}

更新数据

更新数据是通过使用 save() 方法来实现的。

use Cake\ORM\Locator\LocatorAwareTrait;

$articlesTable = $this->fetchTable('Articles');
$article = $articlesTable->get(12); // Return article with id 12

$article->title = 'CakePHP is THE best PHP framework!';
$articlesTable->save($article);

CakePHP 会根据 isNew() 方法的返回值来确定是执行插入操作还是更新操作。通过 get()find() 检索的实体在调用其 isNew() 方法时总是返回 false

使用关联保存

默认情况下,save() 方法也会保存一层关联。

$articlesTable = $this->fetchTable('Articles');
$author = $articlesTable->Authors->findByUserName('mark')->first();

$article = $articlesTable->newEmptyEntity();
$article->title = 'An article by mark';
$article->author = $author;

if ($articlesTable->save($article)) {
    // The foreign key value was set automatically.
    echo $article->author_id;
}

save() 方法也可以为关联创建新记录。

$firstComment = $articlesTable->Comments->newEmptyEntity();
$firstComment->body = 'The CakePHP features are outstanding';

$secondComment = $articlesTable->Comments->newEmptyEntity();
$secondComment->body = 'CakePHP performance is terrific!';

$tag1 = $articlesTable->Tags->findByName('cakephp')->first();
$tag2 = $articlesTable->Tags->newEmptyEntity();
$tag2->name = 'awesome';

$article = $articlesTable->get(12);
$article->comments = [$firstComment, $secondComment];
$article->tags = [$tag1, $tag2];

$articlesTable->save($article);

关联多对多记录

前面的示例演示了如何将一些标签关联到一篇文章。另一种实现相同目标的方法是使用关联中的 link() 方法。

$tag1 = $articlesTable->Tags->findByName('cakephp')->first();
$tag2 = $articlesTable->Tags->newEmptyEntity();
$tag2->name = 'awesome';

$articlesTable->Tags->link($article, [$tag1, $tag2]);

将请求数据转换为实体

在编辑和保存数据回数据库之前,您需要将请求数据从请求中保存的数组格式转换为 ORM 使用的实体。Table 类提供了一种有效的方法来将一个或多个实体从请求数据中转换。您可以使用以下方法转换单个实体:

// In a controller

$articles = $this->fetchTable('Articles');

// Validate and convert to an Entity object
$entity = $articles->newEntity($this->request->getData());

注意

如果您正在使用 newEntity() 并且生成的实体缺少一些或全部传递给它们的数据,请仔细检查您要设置的列是否列在实体的 $_accessible 属性中。请参阅大规模赋值

请求数据应该遵循您的实体的结构。例如,如果您有一篇文章,它属于一个用户,并且有许多评论,那么您的请求数据应该类似于

$data = [
    'title' => 'CakePHP For the Win',
    'body' => 'Baking with CakePHP makes web development fun!',
    'user_id' => 1,
    'user' => [
        'username' => 'mark',
    ],
    'comments' => [
        ['body' => 'The CakePHP features are outstanding'],
        ['body' => 'CakePHP performance is terrific!'],
    ]
];

默认情况下,newEntity() 方法会验证传递给它的数据,如在构建实体之前验证数据部分所述。如果您希望绕过数据验证,请传递 'validate' => false 选项。

$entity = $articles->newEntity($data, ['validate' => false]);

在构建保存嵌套关联的表单时,您需要定义哪些关联应该被编组。

// In a controller

$articles = $this->fetchTable('Articles');

// New entity with nested associations
$entity = $articles->newEntity($this->request->getData(), [
    'associated' => [
        'Tags', 'Comments' => ['associated' => ['Users']],
    ]
]);

以上表明应该编组 Comments 的 ‘Tags’、‘Comments’ 和 ‘Users’。或者,您可以使用点符号来简化。

// In a controller

$articles = $this->fetchTable('Articles');

// New entity with nested associations using dot notation
$entity = $articles->newEntity($this->request->getData(), [
    'associated' => ['Tags', 'Comments.Users'],
]);

您也可以禁用可能的嵌套关联的编组,如下所示。

$entity = $articles->newEntity($data, ['associated' => []]);
// or...
$entity = $articles->patchEntity($entity, $data, ['associated' => []]);

关联数据默认也会被验证,除非另行说明。您也可以更改每个关联要使用的验证集。

// In a controller

$articles = $this->fetchTable('Articles');

// Bypass validation on Tags association and
// Designate 'signup' validation set for Comments.Users
$entity = $articles->newEntity($this->request->getData(), [
    'associated' => [
        'Tags' => ['validate' => false],
        'Comments.Users' => ['validate' => 'signup'],
    ],
]);

使用不同的验证集进行关联 章节提供了有关如何使用不同的验证器进行关联编组的更多信息。

下图概述了 newEntity()patchEntity() 方法内部发生的事情。

Flow diagram showing the marshalling/validation process.

您始终可以依靠从 newEntity() 获取实体。如果验证失败,您的实体将包含错误,并且任何无效的字段都不会在创建的实体中填充。

转换 BelongsToMany 数据

如果您正在保存 belongsToMany 关联,则可以使用实体数据列表或 id 列表。使用实体数据列表时,您的请求数据应该类似于

$data = [
    'title' => 'My title',
    'body' => 'The text',
    'user_id' => 1,
    'tags' => [
        ['name' => 'CakePHP'],
        ['name' => 'Internet'],
    ],
];

以上将创建 2 个新的标签。如果您想将一篇文章与现有标签关联起来,您可以使用 id 列表。您的请求数据应该类似于

$data = [
    'title' => 'My title',
    'body' => 'The text',
    'user_id' => 1,
    'tags' => [
        '_ids' => [1, 2, 3, 4],
    ],
];

如果您需要与一些现有的 belongsToMany 记录关联,并同时创建新的记录,您可以使用扩展格式。

$data = [
    'title' => 'My title',
    'body' => 'The text',
    'user_id' => 1,
    'tags' => [
        ['name' => 'A new tag'],
        ['name' => 'Another new tag'],
        ['id' => 5],
        ['id' => 21],
    ],
];

当以上数据被转换为实体时,您将拥有 4 个标签。前两个将是新对象,后两个将是对现有记录的引用。

在转换 belongsToMany 数据时,您可以使用 onlyIds 选项来禁用实体创建。

$result = $articles->patchEntity($entity, $data, [
    'associated' => ['Tags' => ['onlyIds' => true]],
]);

使用此选项时,它会将 belongsToMany 关联编组限制为仅使用 _ids 数据。

转换 HasMany 数据

如果您想更新现有的 hasMany 关联并更新它们的属性,您应该首先确保您的实体已加载,并且 hasMany 关联已填充。然后,您可以使用类似于以下的请求数据。

$data = [
    'title' => 'My Title',
    'body' => 'The text',
    'comments' => [
        ['id' => 1, 'comment' => 'Update the first comment'],
        ['id' => 2, 'comment' => 'Update the second comment'],
        ['comment' => 'Create a new comment'],
    ],
];

如果您正在保存 hasMany 关联,并且想将现有记录链接到新的父记录,您可以使用 _ids 格式。

$data = [
    'title' => 'My new article',
    'body' => 'The text',
    'user_id' => 1,
    'comments' => [
        '_ids' => [1, 2, 3, 4],
    ],
];

在转换 hasMany 数据时,您可以使用 onlyIds 选项来禁用新实体创建。启用此选项时,它会将 hasMany 编组限制为仅使用 _ids 键,并忽略所有其他数据。

转换多个记录

在创建一次性创建/更新多个记录的表单时,您可以使用 newEntities()

// In a controller.

$articles = $this->fetchTable('Articles');
$entities = $articles->newEntities($this->request->getData());

在这种情况下,多个文章的请求数据应该如下所示

$data = [
    [
        'title' => 'First post',
        'published' => 1,
    ],
    [
        'title' => 'Second post',
        'published' => 1,
    ],
];

将请求数据转换为实体后,您可以保存

// In a controller.
foreach ($entities as $entity) {
    // Save entity
    $articles->save($entity);
}

以上操作将为每个保存的实体运行一个单独的事务。如果您希望将所有实体作为一个事务处理,您可以使用 saveMany()saveManyOrFail()

// Get a boolean indicating success
$articles->saveMany($entities);

// Get a PersistenceFailedException if any records fail to save.
$articles->saveManyOrFail($entities);

更改可访问字段

也可以允许 newEntity() 写入不可访问的字段。例如,id 通常不在 _accessible 属性中。在这种情况下,您可以使用 accessibleFields 选项。这对于保留关联实体的 ID 可能很有用

// In a controller

$articles = $this->fetchTable('Articles');
$entity = $articles->newEntity($this->request->getData(), [
    'associated' => [
        'Tags', 'Comments' => [
            'associated' => [
                'Users' => [
                    'accessibleFields' => ['id' => true],
                ],
            ],
        ],
    ],
]);

以上操作将保持评论和用户之间关联实体不变。

注意

如果您正在使用 newEntity() 并且生成的实体缺少一些或全部传递给它们的数据,请仔细检查您要设置的列是否列在实体的 $_accessible 属性中。请参阅大规模赋值

将请求数据合并到实体中

为了更新实体,您可以选择将请求数据直接应用于现有实体。这样做的好处是,只有实际更改的字段才会被保存,而不是将所有字段发送到数据库以进行持久化。您可以使用 patchEntity() 方法将原始数据的数组合并到现有实体中

// In a controller.

$articles = $this->fetchTable('Articles');
$article = $articles->get(1);
$articles->patchEntity($article, $this->request->getData());
$articles->save($article);

验证和 patchEntity

newEntity() 类似,patchEntity 方法会在将数据复制到实体之前验证数据。该机制在 在构建实体之前验证数据 部分进行了说明。如果您希望在修补实体时禁用验证,请按如下方式传递 validate 选项

// In a controller.

$articles = $this->fetchTable('Articles');
$article = $articles->get(1);
$articles->patchEntity($article, $data, ['validate' => false]);

您还可以更改用于实体或任何关联的验证集

$articles->patchEntity($article, $this->request->getData(), [
    'validate' => 'custom',
    'associated' => ['Tags', 'Comments.Users' => ['validate' => 'signup']]
]);

修补 HasMany 和 BelongsToMany

如上一节所述,请求数据应遵循实体的结构。 patchEntity() 方法同样能够合并关联,默认情况下只合并第一级关联,但如果您希望控制要合并的关联列表或合并更深层级的关联,您可以使用该方法的第三个参数

// In a controller.
$associated = ['Tags', 'Comments.Users'];
// or using nested arrays
$associated = ['Tags', 'Comments' => ['associated' => ['Users']]];
$article = $articles->get(1, ['contain' => $associated]);
$articles->patchEntity($article, $this->request->getData(), [
    'associated' => $associated,
]);
$articles->save($article);

关联是通过将源实体中的主键字段与数据数组中的对应字段进行匹配来合并的。如果在关联的目标属性中没有找到先前存在的实体,关联将构造新的实体。

例如,给出以下请求数据

$data = [
    'title' => 'My title',
    'user' => [
        'username' => 'mark',
    ],
];

尝试修补没有用户属性的实体将创建一个新的用户实体

// In a controller.
$entity = $articles->patchEntity(new Article, $data);
echo $entity->user->username; // Echoes 'mark'

hasMany 和 belongsToMany 关联也是如此,但有一个重要的注意事项

注意

对于 belongsToMany 关联,请确保相关实体有一个可访问的属性,用于关联的实体。

如果一个 Product belongsToMany Tag

// in the Product Entity
protected array $_accessible = [
    // .. other properties
   'tags' => true,
];

注意

对于 hasMany 和 belongsToMany 关联,如果存在任何无法通过主键与数据数组中的记录进行匹配的实体,那么这些记录将从结果实体中丢弃。

请记住,使用 patchEntity()patchEntities() 不会持久化数据,它只是编辑(或创建)给定的实体。为了保存实体,您必须调用表的 save() 方法。

例如,考虑以下情况

$data = [
    'title' => 'My title',
    'body' => 'The text',
    'comments' => [
        ['body' => 'First comment', 'id' => 1],
        ['body' => 'Second comment', 'id' => 2],
    ],
];
$entity = $articles->newEntity($data);
$articles->save($entity);

$newData = [
    'comments' => [
        ['body' => 'Changed comment', 'id' => 1],
        ['body' => 'A new comment'],
    ],
];
$articles->patchEntity($entity, $newData);
$articles->save($entity);

最后,如果实体被转换回数组,您将获得以下结果

[
    'title' => 'My title',
    'body' => 'The text',
    'comments' => [
        ['body' => 'Changed comment', 'id' => 1],
        ['body' => 'A new comment'],
    ],
];

如您所见,id 为 2 的评论不再存在,因为它无法与 $newData 数组中的任何内容匹配。之所以发生这种情况,是因为 CakePHP 反映了请求数据中描述的新状态。

这种方法的一些额外优势是,它减少了再次持久化实体时需要执行的操作数量。

请注意,这并不意味着 id 为 2 的评论已从数据库中删除,如果您希望删除该文章中实体中不存在的评论,您可以收集主键并对不在列表中的主键执行批量删除

// In a controller.
use Cake\Collection\Collection;

$comments = $this->fetchTable('Comments');
$present = (new Collection($entity->comments))->extract('id')->filter()->toList();
$comments->deleteAll([
    'article_id' => $article->id,
    'id NOT IN' => $present,
]);

如您所见,这也帮助创建了需要像单个集合一样实现关联的解决方案。

您也可以一次修补多个实体。对修补 hasMany 和 belongsToMany 关联所做的考虑适用于修补多个实体:匹配是通过主键字段值进行的,原始实体数组中缺少的匹配项将被删除,并且不会出现在结果中

// In a controller.

$articles = $this->fetchTable('Articles');
$list = $articles->find('popular')->toList();
$patched = $articles->patchEntities($list, $this->request->getData());
foreach ($patched as $entity) {
    $articles->save($entity);
}

与使用 patchEntity() 类似,您可以使用第三个参数来控制将合并到数组中每个实体的关联

// In a controller.
$patched = $articles->patchEntities(
    $list,
    $this->request->getData(),
    ['associated' => ['Tags', 'Comments.Users']]
);

在构建实体之前修改请求数据

如果您需要在将请求数据转换为实体之前修改请求数据,可以使用 Model.beforeMarshal 事件。此事件允许您在创建实体之前操作请求数据

// Include use statements at the top of your file.
use Cake\Event\EventInterface;
use ArrayObject;

// In a table or behavior class
public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options)
{
    if (isset($data['username'])) {
        $data['username'] = mb_strtolower($data['username']);
    }
}

$data 参数是一个 ArrayObject 实例,因此您不必返回它来更改用于创建实体的数据。

beforeMarshal 的主要目的是帮助用户通过验证过程,当简单的错误可以自动解决时,或者当需要重新构建数据以便将其放入正确的字段中时。

Model.beforeMarshal 事件在验证过程开始时触发,原因之一是 beforeMarshal 允许更改验证规则和保存选项,例如字段列表。验证在该事件完成后立即触发。更改数据在验证之前的一个常见示例是在保存之前修剪所有字段

// Include use statements at the top of your file.
use Cake\Event\EventInterface;
use ArrayObject;

// In a table or behavior class
public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options)
{
    foreach ($data as $key => $value) {
        if (is_string($value)) {
            $data[$key] = trim($value);
        }
    }
}

由于编组过程的工作方式,如果字段未通过验证,它将自动从数据数组中删除,并且不会被复制到实体中。这是为了防止不一致的数据进入实体对象。

此外, beforeMarshal 中的数据是传递数据的副本。这是因为必须保留原始用户输入,因为它可能在其他地方使用。

从请求数据更新后修改实体

Model.afterMarshal 事件允许您在从请求数据创建或更新实体后修改实体。它可以用来应用无法通过 Validator 方法轻松表达的额外验证逻辑

// Include use statements at the top of your file.
use Cake\Event\EventInterface;
use Cake\ORM\EntityInterface;
use ArrayObject;

// In a table or behavior class
public function afterMarshal(
    EventInterface $event,
    EntityInterface $entity,
    ArrayObject $data,
    ArrayObject $options
) {
    // Don't accept people who have a name starting with J on the 20th
    // of each month.
    if (mb_substr($entity->name, 1) === 'J' && (int)date('d') === 20) {
        $entity->setError('name', 'No J people today sorry.');
    }
}

在构建实体之前验证数据

验证数据 章提供了有关如何使用 CakePHP 的验证功能来确保数据保持正确和一致的更多信息。

避免属性批量分配攻击

在从请求数据创建或合并实体时,您需要小心您允许用户更改或添加到实体中的内容。例如,通过在请求中发送包含 user_id 的数组,攻击者可以更改文章的所有者,从而导致不良影响

// Contains ['user_id' => 100, 'title' => 'Hacked!'];
$data = $this->request->getData();
$entity = $this->patchEntity($entity, $data);
$this->save($entity);

有两种方法可以保护您免受此问题的侵害。第一种方法是使用实体中的 批量分配 功能设置可以安全地从请求中设置的默认列。

第二种方法是在创建或合并数据到实体时使用 fields 选项

// Contains ['user_id' => 100, 'title' => 'Hacked!'];
$data = $this->request->getData();

// Only allow title to be changed
$entity = $this->patchEntity($entity, $data, [
    'fields' => ['title']
]);
$this->save($entity);

您还可以控制可以为关联分配的属性

// Only allow changing the title and tags
// and the tag name is the only column that can be set
$entity = $this->patchEntity($entity, $data, [
    'fields' => ['title', 'tags'],
    'associated' => ['Tags' => ['fields' => ['name']]]
]);
$this->save($entity);

当您有许多不同的功能供用户访问,并且您希望让用户根据其权限编辑不同的数据时,使用此功能非常方便。

保存实体

Cake\ORM\Table::save(Entity $entity, array $options = [])

将请求数据保存到数据库时,您需要首先使用 newEntity() 填充一个新实体,以传递到 save() 中。例如

// In a controller

$articles = $this->fetchTable('Articles');
$article = $articles->newEntity($this->request->getData());
if ($articles->save($article)) {
    // ...
}

ORM 使用实体上的 isNew() 方法来确定应该执行插入还是更新。如果 isNew() 方法返回 true 并且实体具有主键值,则将发出“exists”查询。“exists”查询可以通过在 $options 参数中传递 'checkExisting' => false 来抑制

$articles->save($article, ['checkExisting' => false]);

加载了一些实体后,您可能希望修改它们并更新数据库。这在 CakePHP 中是一个非常简单的练习

$articles = $this->fetchTable('Articles');
$article = $articles->find('all')->where(['id' => 2])->first();

$article->title = 'My new title';
$articles->save($article);

保存时,CakePHP 将 应用您的规则,并将保存操作包装在数据库事务中。它还会只更新已更改的属性。上面的 save() 调用将生成类似的 SQL

UPDATE articles SET title = 'My new title' WHERE id = 2;

如果您有一个新实体,将生成以下 SQL

INSERT INTO articles (title) VALUES ('My new title');

保存实体时,会发生以下几件事

  1. 如果未禁用,将启动规则检查。

  2. 规则检查将触发 Model.beforeRules 事件。如果此事件停止,保存操作将失败并返回 false

  3. 将检查规则。如果实体正在创建,将使用 create 规则。如果实体正在更新,将使用 update 规则。

  4. 将触发 Model.afterRules 事件。

  5. 将触发 Model.beforeSave 事件。如果事件被阻止,保存操作将被中止,并且 save() 方法将返回 false

  6. 保存父关联。例如,任何列出的 belongsTo 关联将被保存。

  7. 实体上的修改字段将被保存。

  8. 保存子关联。例如,任何列出的 hasMany、 hasOne 或 belongsToMany 关联将被保存。

  9. 将触发 Model.afterSave 事件。

  10. 将触发 Model.afterSaveCommit 事件。

以下图表说明了上述过程

Flow diagram showing the save process.

有关创建和使用规则的更多信息,请参阅 应用应用程序规则 部分。

警告

如果在保存实体时没有对实体进行任何更改,则回调不会触发,因为没有执行保存操作。

如果保存成功,save() 方法将返回修改后的实体,如果失败,则返回 false。您可以使用 save 的 $options 参数来禁用规则和/或事务

// In a controller or table method.
$articles->save($article, ['checkRules' => false, 'atomic' => false]);

保存关联

保存实体时,您也可以选择保存部分或全部关联实体。默认情况下,所有一级实体都将被保存。例如,保存一篇文章,也将自动更新与文章表直接相关的任何脏实体。

您可以使用 associated 选项来微调要保存的关联。

// In a controller.

// Only save the comments association
$articles->save($entity, ['associated' => ['Comments']]);

您可以使用点表示法来定义保存远程或深度嵌套的关联。

// Save the company, the employees and related addresses for each of them.
$companies->save($entity, ['associated' => ['Employees.Addresses']]);

此外,您可以将关联的点表示法与选项数组结合使用。

$companies->save($entity, [
  'associated' => [
    'Employees',
    'Employees.Addresses'
  ]
]);

您的实体结构应与从数据库中加载时相同。有关如何为关联构建输入的更多信息,请参阅表单助手文档的 关联表单输入 部分。

如果在构建实体后构建或修改关联数据,则必须使用 setDirty() 将关联属性标记为已修改。

$company->author->name = 'Master Chef';
$company->setDirty('author', true);

保存 BelongsTo 关联

保存 belongsTo 关联时,ORM 预期一个名为关联名称的单数、下划线 版本的嵌套实体。例如

// In a controller.
$data = [
    'title' => 'First Post',
    'user' => [
        'id' => 1,
        'username' => 'mark',
    ],
];

$articles = $this->fetchTable('Articles');
$article = $articles->newEntity($data, [
    'associated' => ['Users']
]);

$articles->save($article);

保存 HasOne 关联

保存 hasOne 关联时,ORM 预期一个名为关联名称的单数、下划线 版本的嵌套实体。例如

// In a controller.
$data = [
    'id' => 1,
    'username' => 'cakephp',
    'profile' => [
        'twitter' => '@cakephp',
    ],
];

$users = $this->fetchTable('Users');
$user = $users->newEntity($data, [
    'associated' => ['Profiles']
]);
$users->save($user);

保存 HasMany 关联

保存 hasMany 关联时,ORM 预期一个名为关联名称的复数、下划线 版本的实体数组。例如

// In a controller.
$data = [
    'title' => 'First Post',
    'comments' => [
        ['body' => 'Best post ever'],
        ['body' => 'I really like this.']
    ]
];

$articles = $this->fetchTable('Articles');
$article = $articles->newEntity($data, [
    'associated' => ['Comments']
]);
$articles->save($article);

保存 hasMany 关联时,关联记录将被更新或插入。对于记录已经在数据库中具有关联记录的情况,您可以选择两种保存策略

追加

关联记录将在数据库中更新,或者如果与任何现有记录不匹配,则将插入。

替换

任何与提供的记录不匹配的现有记录都将从数据库中删除。只有提供的记录将保留(或插入)。

默认情况下,将使用 append 保存策略。有关定义 saveStrategy 的详细信息,请参阅 HasMany 关联

无论何时将新记录添加到现有关联,您都应始终将关联属性标记为“脏”。这使 ORM 知道必须持久化关联属性。

$article->comments[] = $comment;
$article->setDirty('comments', true);

如果没有调用 setDirty(),则更新的评论将不会保存。

如果要创建一个新实体,并且要将现有记录添加到 has many/belongs to many 关联,则需要首先初始化关联属性

$article->comments = [];

如果没有初始化,调用 $article->comments[] = $comment; 将不会有任何效果。

保存 BelongsToMany 关联

保存 belongsToMany 关联时,ORM 预期一个名为关联名称的复数、下划线 版本的实体数组。例如

// In a controller.
$data = [
    'title' => 'First Post',
    'tags' => [
        ['tag' => 'CakePHP'],
        ['tag' => 'Framework']
    ]
];

$articles = $this->fetchTable('Articles');
$article = $articles->newEntity($data, [
    'associated' => ['Tags']
]);
$articles->save($article);

将请求数据转换为实体时,newEntity()newEntities() 方法将处理属性数组以及 _ids 键处的 ID 列表。使用 _ids 键可以为 belongs to many 关联构建选择框或基于复选框的表单控件。有关更多信息,请参阅 将请求数据转换为实体 部分。

保存 belongsToMany 关联时,您可以选择两种保存策略

追加

仅在该关联的每一侧之间创建新的链接。即使这些链接可能不存在于要保存的实体数组中,此策略也不会破坏现有的链接。

替换

保存时,将删除现有链接,并在联接表中创建新的链接。如果数据库中存在指向要保存的一些实体的现有链接,则这些链接将被更新,而不是删除然后重新保存。

有关定义 saveStrategy 的详细信息,请参阅 BelongsToMany 关联

默认情况下,将使用 replace 策略。无论何时将新记录添加到现有关联,您都应始终将关联属性标记为“脏”。这使 ORM 知道必须持久化关联属性。

$article->tags[] = $tag;
$article->setDirty('tags', true);

如果没有调用 setDirty(),则更新的标签将不会保存。

通常,您会发现自己想在两个现有实体之间建立关联,例如,用户共同创作一篇文章。这是通过使用 link() 方法完成的,如下所示

$article = $this->Articles->get($articleId);
$user = $this->Users->get($userId);

$this->Articles->Users->link($article, [$user]);

保存 belongsToMany 关联时,保存一些额外数据到联接表可能很重要。在前面关于标签的示例中,它可能是对该文章投票的人的 vote_typevote_type 可以是 upvotedownvote,并且由字符串表示。该关系在用户和文章之间。

保存该关联以及 vote_type 是通过首先向 _joinData 添加一些数据,然后使用 link() 保存关联来完成的,示例如下

$article = $this->Articles->get($articleId);
$user = $this->Users->get($userId);

$user->_joinData = new Entity(['vote_type' => $voteType], ['markNew' => true]);
$this->Articles->Users->link($article, [$user]);

将额外数据保存到联接表

在某些情况下,联接 BelongsToMany 关联的表将包含其他列。CakePHP 使将属性保存到这些列变得非常简单。belongsToMany 关联中的每个实体都有一个 _joinData 属性,该属性包含联接表上的其他列。此数据可以是数组或实体实例。例如,如果学生 BelongsToMany 课程,那么我们可以有一个看起来像这样的联接表

id | student_id | course_id | days_attended | grade

保存数据时,可以通过将数据设置为 _joinData 属性来填充联接表上的其他列

$student->courses[0]->_joinData->grade = 80.12;
$student->courses[0]->_joinData->days_attended = 30;

$studentsTable->save($student);

上面的示例仅在 _joinData 属性已经是对联接表实体的引用时才有效。如果您还没有 _joinData 实体,则可以使用 newEntity() 创建一个实体

$coursesMembershipsTable = $this->fetchTable('CoursesMemberships');
$student->courses[0]->_joinData = $coursesMembershipsTable->newEntity([
    'grade' => 80.12,
    'days_attended' => 30
]);

$studentsTable->save($student);

如果要保存从请求数据构建的实体,则 _joinData 属性可以是实体,也可以是数据数组。从请求数据保存联接表数据时,您的 POST 数据应如下所示

$data = [
    'first_name' => 'Sally',
    'last_name' => 'Parker',
    'courses' => [
        [
            'id' => 10,
            '_joinData' => [
                'grade' => 80.12,
                'days_attended' => 30
            ]
        ],
        // Other courses.
    ]
];
$student = $this->Students->newEntity($data, [
    'associated' => ['Courses._joinData']
]);

有关如何使用 FormHelper 正确构建输入的更多信息,请参阅 创建关联数据的输入 文档。

保存复杂类型

表能够存储以基本类型表示的数据,例如字符串、整数、浮点数、布尔值等。但它也可以扩展为接受更复杂的类型,例如数组或对象,并将这些数据序列化为可以保存在数据库中的更简单的类型。

此功能是通过使用自定义类型系统实现的。有关如何构建自定义列类型的更多信息,请参阅 添加自定义类型 部分。

use Cake\Database\TypeFactory;

TypeFactory::map('json', 'Cake\Database\Type\JsonType');

// In src/Model/Table/UsersTable.php

class UsersTable extends Table
{
    public function getSchema(): TableSchemaInterface
    {
        $schema = parent::getSchema();
        $schema->setColumnType('preferences', 'json');

        return $schema;
    }
}

上面的代码将 preferences 列映射到 json 自定义类型。这意味着在检索该列的数据时,它将从数据库中的 JSON 字符串反序列化并作为数组放入实体中。

同样,在保存时,数组将转换回其 JSON 表示形式。

$user = new User([
    'preferences' => [
        'sports' => ['football', 'baseball'],
        'books' => ['Mastering PHP', 'Hamlet']
    ]
]);
$usersTable->save($user);

使用复杂类型时,重要的是要验证从最终用户接收到的数据是否为正确类型。未能正确处理复杂数据会导致恶意用户能够存储他们通常无法存储的数据。

严格保存

Cake\ORM\Table::saveOrFail($entity, $options = [])

使用此方法将在以下情况下抛出 Cake\ORM\Exception\PersistenceFailedException

  • 应用程序规则检查失败

  • 实体包含错误

  • 保存被回调中止。

当您在没有人工监控的情况下执行复杂的数据库操作时,例如在 Shell 任务中,使用此方法会很有帮助。

注意

如果您在控制器中使用此方法,请确保捕获可能引发的 PersistenceFailedException

如果您想找出无法保存的实体,可以使用 Cake\ORMException\PersistenceFailedException::getEntity() 方法。

try {
    $table->saveOrFail($entity);
} catch (\Cake\ORM\Exception\PersistenceFailedException $e) {
    echo $e->getEntity();
}

由于这在内部执行 Cake\ORM\Table::save() 调用,因此所有相应的保存事件都将被触发。

查找或创建实体

Cake\ORM\Table::findOrCreate($search, $callback = null, $options = [])

根据 $search 查找现有记录,或使用 $search 中的属性并调用可选的 $callback 创建新记录。此方法非常适合您需要降低重复记录可能性的场景。

$record = $table->findOrCreate(
    ['email' => '[email protected]'],
    function ($entity) use ($otherData) {
        // Only called when a new record is created.
        $entity->name = $otherData['name'];
    }
);

如果您的查找条件需要自定义排序、关联或条件,那么 $search 参数可以是可调用对象或 SelectQuery 对象。如果您使用可调用对象,它应该以 SelectQuery 作为其参数。

如果返回的实体是新记录,则它将已被保存。此方法支持的选项是

  • atomic 查找和保存操作是否应在事务内完成。

  • defaults 设置为 false 以不在创建的实体中设置 $search 属性。

使用现有主键创建

在处理 UUID 主键时,您通常希望提供一个外部生成的 value,而不是让系统为您生成标识符。

在这种情况下,请确保您没有将主键作为编组数据的组成部分传递。相反,分配主键,然后将剩余的实体数据修补进去。

$record = $table->newEmptyEntity();
$record->id = $existingUuid;
$record = $table->patchEntity($record, $existingData);
$table->saveOrFail($record);

保存多个实体

Cake\ORM\Table::saveMany($entities, $options = [])

使用此方法,您可以原子地保存多个实体。 $entities 可以是使用 newEntities() / patchEntities() 创建的实体数组。 $options 可以具有与 save() 接受的相同的选项。

$data = [
    [
        'title' => 'First post',
        'published' => 1
    ],
    [
        'title' => 'Second post',
        'published' => 1
    ],
];

$articles = $this->fetchTable('Articles');
$entities = $articles->newEntities($data);
$result = $articles->saveMany($entities);

结果将在成功时为更新的实体,或在失败时为 false

批量更新

Cake\ORM\Table::updateAll($fields, $conditions)

可能有时单独更新行效率低下或没有必要。在这些情况下,使用批量更新一次修改多行更有效,方法是分配新的字段值以及更新的条件。

// Publish all the unpublished articles.
function publishAllUnpublished()
{
    $this->updateAll(
        [  // fields
            'published' => true,
            'publish_date' => DateTime::now()
        ],
        [  // conditions
            'published' => false
        ]
    );
}

如果您需要进行批量更新并使用 SQL 表达式,则需要使用表达式对象,因为 updateAll() 在幕后使用准备好的语句。

use Cake\Database\Expression\QueryExpression;

...

function incrementCounters()
{
    $expression = new QueryExpression('view_count = view_count + 1');
    $this->updateAll([$expression], ['published' => true]);
}

如果更新了 1 行或更多行,则批量更新将被视为成功。

警告

updateAll *不会* 触发 beforeSave/afterSave 事件。如果您需要这些,请先加载记录集合并更新它们。

updateAll() 仅用于方便起见。您也可以使用此更灵活的界面。

// Publish all the unpublished articles.
function publishAllUnpublished()
{
    $this->updateQuery()
        ->set(['published' => true])
        ->where(['published' => false])
        ->execute();
}

另请参见:更新数据.