检索数据和结果集

class Cake\ORM\Table

虽然表对象提供了对“存储库”或对象集合的抽象,但当你查询单个记录时,你会得到“实体”对象。虽然本节讨论了查找和加载实体的不同方法,但你应该阅读 实体 部分以获取有关实体的更多信息。

调试查询和结果集

由于 ORM 现在返回集合和实体,调试这些对象可能比在以前版本的 CakePHP 中更复杂。现在有各种方法可以检查 ORM 返回的数据。

  • debug($query) 显示 SQL 和绑定参数,不显示结果。

  • sql($query) 显示安装 DebugKit 后的最终渲染 SQL。

  • debug($query->all()) 显示 ResultSet 属性(不显示结果)。

  • debug($query->toList()) 以数组形式显示结果。

  • debug(iterator_to_array($query)) 以数组格式显示查询结果。

  • debug(json_encode($query, JSON_PRETTY_PRINT)) 更易于理解的结果。

  • debug($query->first()) 显示单个实体的属性。

  • debug((string)$query->first()) 以 JSON 格式显示单个实体的属性。

通过主键获取单个实体

Cake\ORM\Table::get($id, $options = [])

在编辑或查看实体及其相关数据时,从数据库中加载单个实体通常很方便。你可以使用 get() 来实现这一点。

// In a controller or table method.

// Get a single article
$article = $articles->get($id);

// Get a single article, and related comments
$article = $articles->get($id, contain: ['Comments']);

如果 get 操作没有找到任何结果,则会引发 Cake\Datasource\Exception\RecordNotFoundException。你可以自己捕获此异常,或者让 CakePHP 将其转换为 404 错误。

find() 一样,get() 也集成了缓存。你可以在调用 get() 时使用 cache 选项来执行读取缓存。

// In a controller or table method.

// Use any cache config or CacheEngine instance & a generated key
$article = $articles->get($id, cache: 'custom');

// Use any cache config or CacheEngine instance & specific key
$article = $articles->get($id, cache: 'custom', key: 'mykey');

// Explicitly disable caching
$article = $articles->get($id, cache: false);

可选地,你可以使用 自定义查找器方法 获取实体。例如,你可能希望获取实体的所有翻译。你可以通过使用 finder 选项来实现这一点。

$article = $articles->get($id, 'translations');

get() 支持的选项列表如下:

  • cache 缓存配置。

  • key 缓存键。

  • finder 自定义查找器函数。

  • conditions 为查询的 WHERE 子句提供条件。

  • limit 设置要获取的行数。

  • offset 设置要获取的页面偏移量。你还可以使用 page 来简化计算。

  • contain 定义要积极加载的关联。

  • fields 限制加载到实体中的字段。仅加载某些字段会导致实体行为不正常。

  • group 向查询添加 GROUP BY 子句。这在使用聚合函数时很有用。

  • having 向查询添加 HAVING 子句。

  • join 定义其他自定义联接。

使用查找器加载数据

Cake\ORM\Table::find($type, mixed ...$args)

在你可以使用实体之前,你需要加载它们。最简单的方法是使用 find() 方法。find 方法提供了一种简短且可扩展的方式来查找你感兴趣的数据。

// In a controller or table method.

// Find all the articles
$query = $articles->find('all');

任何 find() 方法的返回值始终是 Cake\ORM\Query\SelectQuery 对象。SelectQuery 类允许你在创建查询后进一步细化查询。SelectQuery 对象是延迟执行的,并且直到你开始获取行、将其转换为数组或调用 all() 方法时才会执行。

// In a controller or table method.

// Find all the articles.
// At this point the query has not run.
$query = $articles->find('all');

// Calling all() will execute the query
// and return the result set.
$results = $query->all();

// Once we have a result set we can get all the rows
$data = $results->toList();

// Converting the query to a key-value array will also execute it.
$data = $query->toArray();

注意

在你启动查询后,可以使用 查询构建器 接口构建更复杂的查询,添加额外的条件、限制或使用流畅的接口包含关联。

// In a controller or table method.
$query = $articles->find('all')
    ->where(['Articles.created >' => new DateTime('-10 days')])
    ->contain(['Comments', 'Authors'])
    ->limit(10);

你还可以向 find() 提供许多常用的选项。

// In a controller or table method.
$query = $articles->find('all',
    conditions: ['Articles.created >' => new DateTime('-10 days')],
    contain: ['Authors', 'Comments'],
    limit: 10
);

find() 默认支持的命名参数列表如下:

  • conditions 为查询的 WHERE 子句提供条件。

  • limit 设置要获取的行数。

  • offset 设置要获取的页面偏移量。你还可以使用 page 来简化计算。

  • contain 定义要积极加载的关联。

  • fields 限制加载到实体中的字段。仅加载某些字段会导致实体行为不正常。

  • group 向查询添加 GROUP BY 子句。这在使用聚合函数时很有用。

  • having 向查询添加 HAVING 子句。

  • join 定义其他自定义联接。

  • order 对结果集进行排序。

不在此列表中的任何选项都将传递给 beforeFind 监听器,它们可以在其中用于修改查询对象。你可以使用查询对象上的 getOptions() 方法来检索所使用的选项。虽然你可以将查询对象传递给你的控制器,但我们建议你将查询打包为 自定义查找器方法。使用自定义查找器方法将使你能够重用查询并使测试更容易。

默认情况下,查询和结果集将返回 实体 对象。你可以通过禁用水合来检索基本数组。

$query->disableHydration();

// $data is ResultSet that contains array data.
$data = $query->all();

获取第一个结果

first() 方法允许你仅从查询中获取第一行。如果查询尚未执行,则会应用 LIMIT 1 子句。

// In a controller or table method.
$query = $articles->find('all', order: ['Articles.created' => 'DESC']);
$row = $query->first();

这种方法替代了以前版本的 CakePHP 中的 find('first')。如果你正在通过主键加载实体,你可能还想使用 get() 方法。

注意

first() 方法如果未找到结果将返回 null

获取结果计数

在你创建查询对象后,你可以使用 count() 方法获取该查询的结果计数。

// In a controller or table method.
$query = $articles->find('all', conditions: ['Articles.title LIKE' => '%Ovens%']);
$number = $query->count();

有关 count() 方法的其他使用方法,请参阅 返回记录的总数

查找键值对

从应用程序数据中生成一个关联数据数组通常很有用。例如,这在创建 <select> 元素时非常有用。CakePHP 提供了一个易于使用的方法来生成数据的“列表”。

// In a controller or table method.
$query = $articles->find('list');
$data = $query->toArray();

// Data now looks like
$data = [
    1 => 'First post',
    2 => 'Second article I wrote',
];

如果没有其他选项,$data 的键将成为您表格的主键,而值将成为表格的“displayField”。表格的默认“displayField”是 titlename。同时,您可以使用表格对象上的 setDisplayField() 方法来配置表格的显示字段。

class ArticlesTable extends Table
{
    public function initialize(array $config): void
    {
        $this->setDisplayField('label');
    }
}

在调用 list 时,您可以分别使用 keyFieldvalueField 选项来配置用于键和值的字段。

// In a controller or table method.
$query = $articles->find('list', keyField: 'slug', valueField: 'label');
$data = $query->toArray();

// Data now looks like
$data = [
    'first-post' => 'First post',
    'second-article-i-wrote' => 'Second article I wrote',
];

结果可以分组为嵌套集。当您想要分组集,或者想要使用 FormHelper 构建 <optgroup> 元素时,这很有用。

// In a controller or table method.
$query = $articles->find('list', keyField: 'slug', valueField: 'label', groupField: 'author_id');
$data = $query->toArray();

// Data now looks like
$data = [
    1 => [
        'first-post' => 'First post',
        'second-article-i-wrote' => 'Second article I wrote',
    ],
    2 => [
        // More data.
    ]
];

您还可以从可以使用联接访问的关联创建列表数据。

$query = $articles->find('list', keyField: 'id', valueField: 'author.name')
    ->contain(['Authors']);

keyFieldvalueFieldgroupField 表达式将对实体属性路径而不是数据库列进行操作。这意味着您可以在 find(list) 的结果中使用虚拟字段。

自定义键值输出

最后,可以使用闭包在列表查找中访问实体访问器方法。

// In your Authors Entity create a virtual field to be used as the displayField:
protected function _getLabel()
{
    return $this->_fields['first_name'] . ' ' . $this->_fields['last_name']
      . ' / ' . __('User ID %s', $this->_fields['user_id']);
}

此示例显示了使用 Author 实体中的 _getLabel() 访问器方法。

// In your finders/controller:
$query = $articles->find('list',
        keyField: 'id',
        valueField: function ($article) {
            return $article->author->get('label');
        }
    )
    ->contain('Authors');

您也可以直接使用列表来获取标签。

// In AuthorsTable::initialize():
$this->setDisplayField('label'); // Will utilize Author::_getLabel()
// In your finders/controller:
$query = $authors->find('list'); // Will utilize AuthorsTable::getDisplayField()

查找线程数据

find('threaded') 查找器返回通过键字段相互连接的嵌套实体。默认情况下,此字段是 parent_id。此查找器允许您访问存储在“邻接列表”样式表中的数据。所有匹配给定 parent_id 的实体都将放置在 children 属性下。

// In a controller or table method.
$query = $comments->find('threaded');

// Expanded default values
$query = $comments->find('threaded',
    keyField: $comments->primaryKey(),
    parentField: 'parent_id'
);
$results = $query->toArray();

echo count($results[0]->children);
echo $results[0]->children[0]->comment;

parentFieldkeyField 键可用于定义将发生线程的字段。

提示

如果您需要管理更高级的数据树,请考虑使用 Tree

自定义查找器方法

上面的示例展示了如何使用内置的 alllist 查找器。但是,您可以并且建议您实现自己的查找器方法。查找器方法是打包常用查询的理想方法,允许您将查询详细信息抽象到易于使用的方法中。查找器方法通过创建遵循 findFoo 约定的方法来定义,其中 Foo 是您要创建的查找器的名称。例如,如果我们要在文章表中添加一个查找器来查找给定用户编写的文章,我们将执行以下操作。

use App\Model\Entity\User;
use Cake\ORM\Query\SelectQuery;
use Cake\ORM\Table;

class ArticlesTable extends Table
{
    public function findOwnedBy(SelectQuery $query, User $user)
    {
        return $query->where(['author_id' => $user->id]);
    }
}

$query = $articles->find('ownedBy', user: $userEntity);

查找器方法可以根据需要修改查询,或者使用 $options 来使用相关的应用程序逻辑自定义查找器操作。您还可以“堆叠”查找器,允许您轻松地表达复杂的查询。假设您同时具有“published”和“recent”查找器,您可以执行以下操作。

$query = $articles->find('published')->find('recent');

虽然到目前为止,所有示例都展示了表格类上的查找器方法,但查找器方法也可以在 Behaviors 上定义。

如果您需要在获取结果后修改结果,您应该使用 使用 Map/Reduce 修改结果 函数来修改结果。映射减少功能替换了 CakePHP 之前版本中找到的“afterFind”回调。

动态查找器

CakePHP 的 ORM 提供动态构建的查找器方法,这些方法允许您表达简单的查询,而无需任何额外的代码。例如,如果您想按用户名查找用户,您可以执行以下操作。

// In a controller
// The following two calls are equal.
$query = $this->Users->findByUsername('joebob');
$query = $this->Users->findAllByUsername('joebob');

使用动态查找器时,您可以对多个字段进行约束。

$query = $users->findAllByUsernameAndApproved('joebob', 1);

您还可以创建 OR 条件。

$query = $users->findAllByUsernameOrEmail('joebob', '[email protected]');

虽然您可以使用 ORAND 条件,但您不能在一个动态查找器中组合两者。其他查询选项(如 contain)也不支持动态查找器。您应该使用 自定义查找器方法 来封装更复杂的查询。最后,您还可以将动态查找器与自定义查找器组合。

$query = $users->findTrollsByUsername('bro');

上面将转换为以下内容。

$users->find('trolls', conditions: ['username' => 'bro']);

从动态查找器获得查询对象后,如果您想要第一个结果,您需要调用 first()

注意

虽然动态查找器使表达查询变得简单,但它们会增加少量开销。您不能从查询对象调用 findBy 方法。使用查找器链时,动态查找器必须先被调用。

检索关联数据

当您想获取关联数据,或者根据关联数据进行过滤时,有两种方法。

  • 使用 CakePHP ORM 查询函数,如 contain()matching()

  • 使用联接函数,如 innerJoin()leftJoin()rightJoin()

当您想加载主模型及其关联数据时,您应该使用 contain()。虽然 contain() 允许您对加载的关联应用其他条件,但您无法根据关联来约束主模型。有关 contain() 的更多详细信息,请查看 通过 Contain 渴望加载关联

当您想根据关联来限制主模型时,您应该使用 matching()。例如,您想加载具有特定标签的所有文章。有关 matching() 的更多详细信息,请查看 通过匹配和联接过滤关联数据

如果您更喜欢使用联接函数,您可以查看 添加联接 以了解更多信息。

通过 Contain 渴望加载关联

默认情况下,CakePHP 在使用 find() 时不会加载任何关联数据。您需要“包含”或渴望加载您希望在结果中加载的每个关联。

渴望加载有助于避免与 ORM 中的延迟加载相关的许多潜在性能问题。渴望加载生成的查询可以更好地利用联接,从而实现更有效的查询。在 CakePHP 中,您使用“contain”方法声明哪些关联应该被渴望加载。

// In a controller or table method.

// As an option to find()
$query = $articles->find('all', contain: ['Authors', 'Comments']);

// As a method on the query object
$query = $articles->find('all');
$query->contain(['Authors', 'Comments']);

上面将加载结果集中每篇文章的相关作者和评论。您可以使用嵌套数组定义要加载的关联来加载嵌套关联。

$query = $articles->find()->contain([
    'Authors' => ['Addresses'], 'Comments' => ['Authors']
]);

或者,您可以使用点表示法表达嵌套关联。

$query = $articles->find()->contain([
    'Authors.Addresses',
    'Comments.Authors'
]);

您可以根据需要加载关联。

$query = $products->find()->contain([
    'Shops.Cities.Countries',
    'Shops.Managers'
]);

这等效于调用。

$query = $products->find()->contain([
    'Shops' => ['Cities.Countries', 'Managers']
]);

您可以使用多个 contain() 语句从所有关联中选择字段。

$query = $this->find()->select([
    'Realestates.id',
    'Realestates.title',
    'Realestates.description'
])
->contain([
    'RealestateAttributes' => [
        'Attributes' => [
            'fields' => [
                // Aliased fields in contain() must include
                // the model prefix to be mapped correctly.
                'Attributes__name' => 'attr_name',
            ],
        ],
    ],
])
->contain([
    'RealestateAttributes' => [
        'fields' => [
            'RealestateAttributes.realestate_id',
            'RealestateAttributes.value',
        ],
    ],
])
->where($condition);

如果您需要重置查询中的包含,您可以将第二个参数设置为 true

$query = $articles->find();
$query->contain(['Authors', 'Comments'], true);

注意

contain() 调用中的关联名称应与关联定义中的关联大小写相同,而不是用于保存关联记录的属性名称。例如,如果您已将关联声明为 belongsTo('Users'),那么您必须使用 contain('Users') 而不是 contain('users')contain('user')

将条件传递给 Contain

使用 contain() 时,您可以限制关联返回的数据并根据条件对其进行过滤。要指定条件,请传递一个匿名函数,该函数将查询对象 \Cake\ORM\Query\SelectQuery 作为第一个参数接收。

// In a controller or table method.
$query = $articles->find()->contain('Comments', function (SelectQuery $q) {
    return $q
        ->select(['body', 'author_id'])
        ->where(['Comments.approved' => true]);
});

这在控制器级别的分页中也有效。

$this->paginate['contain'] = [
    'Comments' => function (SelectQuery $query) {
        return $query->select(['body', 'author_id'])
        ->where(['Comments.approved' => true]);
    }
];

警告

如果结果缺少关联实体,请确保在查询中选择了外键列。没有外键,ORM 就无法找到匹配的行。

也可以使用点表示法来限制深度嵌套的关联。

$query = $articles->find()->contain([
    'Comments',
    'Authors.Profiles' => function (SelectQuery $q) {
        return $q->where(['Profiles.is_published' => true]);
    }
]);

在上面的示例中,即使作者没有已发布的配置文件,您仍然会获得作者。要只获取具有已发布配置文件的作者,请使用 matching()。如果您在关联中定义了自定义查找器,您可以在 contain() 中使用它们。

// Bring all articles, but only bring the comments that are approved and
// popular.
$query = $articles->find()->contain('Comments', function (SelectQuery $q) {
    return $q->find('approved')->find('popular');
});

注意

对于 BelongsToHasOne 关联,仅 selectwhere 子句在 contain() 查询中有效。对于 HasManyBelongsToMany,所有子句,例如 order() 都是有效的。

您可以控制 contain() 使用的查询子句,而不仅仅是查询子句。如果传递包含关联的数组,您可以覆盖 foreignKeyjoinTypestrategy。有关每个关联类型的默认值和选项的详细信息,请参阅 关联 - 链接表

您可以将 false 作为新的 foreignKey 传递,以完全禁用外键约束。使用 queryBuilder 选项在使用数组时自定义查询。

$query = $articles->find()->contain([
    'Authors' => [
        'foreignKey' => false,
        'queryBuilder' => function (SelectQuery $q) {
            return $q->where(/* ... */); // Full conditions for filtering
        }
    ]
]);

如果您使用 select() 限制了要加载的字段,但还想加载包含的关联中的字段,可以将关联对象传递给 select()

// Select id & title from articles, but all fields off of Users.
$query = $articles->find()
    ->select(['id', 'title'])
    ->select($articles->Users)
    ->contain(['Users']);

或者,您可以在匿名函数中使用 enableAutoFields()

// Select id & title from articles, but all fields off of Users.
$query = $articles->find()
    ->select(['id', 'title'])
    ->contain(['Users' => function(SelectQuery $q) {
        return $q->enableAutoFields();
    }]);

排序包含的关联

在加载 HasMany 和 BelongsToMany 关联时,可以使用 sort 选项对这些关联中的数据进行排序。

$query->contain([
    'Comments' => [
        'sort' => ['Comments.created' => 'DESC']
    ]
]);

通过匹配和连接筛选关联数据

关联的一个非常常见的查询用例是查找与特定关联数据“匹配”的记录。例如,如果您有“Articles belongsToMany Tags”,您可能想要查找包含 CakePHP 标签的文章。使用 CakePHP 中的 ORM,这非常简单。

// In a controller or table method.

$query = $articles->find();
$query->matching('Tags', function ($q) {
    return $q->where(['Tags.name' => 'CakePHP']);
});

您也可以将此策略应用于 HasMany 关联。例如,如果“Authors HasMany Articles”,您可以使用以下代码查找所有拥有最近发布的文章的作者。

$query = $authors->find();
$query->matching('Articles', function ($q) {
    return $q->where(['Articles.created >=' => new DateTime('-10 days')]);
});

通过深度关联进行筛选使用与 contain() 相同的可预测语法。

// In a controller or table method.
$query = $products->find()->matching(
    'Shops.Cities.Countries', function ($q) {
        return $q->where(['Countries.name' => 'Japan']);
    }
);

// Bring unique articles that were commented by `markstory` using passed variable
// Dotted matching paths should be used over nested matching() calls
$username = 'markstory';
$query = $articles->find()->matching('Comments.Users', function ($q) use ($username) {
    return $q->where(['username' => $username]);
});

注意

由于此函数将创建一个 INNER JOIN,因此您可能需要考虑在查找查询中调用 distinct,因为如果您的条件没有排除重复行,您可能会得到重复行。例如,当同一个用户对同一篇文章多次评论时,可能会出现这种情况。

与关联“匹配”的数据将在实体的 _matchingData 属性中可用。如果匹配和包含具有相同的关联,您可以预期在结果中获得 _matchingData 和标准关联属性。

使用 innerJoinWith

有时您需要匹配特定的关联数据,但实际上并不需要像 matching() 那样加载匹配的记录。您可以使用 innerJoinWith() 创建 matching() 使用的 INNER JOIN

$query = $articles->find();
$query->innerJoinWith('Tags', function ($q) {
    return $q->where(['Tags.name' => 'CakePHP']);
});

innerJoinWith() 允许您使用相同的参数和点表示法。

$query = $products->find()->innerJoinWith(
    'Shops.Cities.Countries', function ($q) {
        return $q->where(['Countries.name' => 'Japan']);
    }
);

当您想要匹配特定记录并一起加载关联数据时,您可以将 innerJoinWith()contain() 与相同的关联结合使用。以下示例匹配包含特定标签的文章,并加载相同的标签。

$filter = ['Tags.name' => 'CakePHP'];
$query = $articles->find()
    ->distinct($articles->getPrimaryKey())
    ->contain('Tags', function (SelectQuery $q) use ($filter) {
        return $q->where($filter);
    })
    ->innerJoinWith('Tags', function (SelectQuery $q) use ($filter) {
        return $q->where($filter);
    });

注意

如果您使用 innerJoinWith() 并想要从该关联中 select() 字段,则需要为该字段使用别名。

$query
    ->select(['country_name' => 'Countries.name'])
    ->innerJoinWith('Countries');

如果您没有使用别名,您将在 _matchingData 中看到数据,如上面 matching() 所述。这是 matching() 不知道您手动选择了该字段的一个边缘情况。

警告

您不应该将 innerJoinWith()matching() 与相同的关联结合使用。这将生成多个 INNER JOIN 语句,并且可能不会创建您期望的查询。

使用 notMatching

matching() 的反面是 notMatching()。此函数将更改查询,以便它筛选与指定关联没有关系的结果。

// In a controller or table method.

$query = $articlesTable
    ->find()
    ->notMatching('Tags', function ($q) {
        return $q->where(['Tags.name' => 'boring']);
    });

上面的示例将找到所有没有使用“boring”一词进行标记的文章。您也可以将此方法应用于 HasMany 关联。例如,您可以查找过去 10 天内没有发表文章的所有作者。

$query = $authorsTable
    ->find()
    ->notMatching('Articles', function ($q) {
        return $q->where(['Articles.created >=' => new \DateTime('-10 days')]);
    });

也可以使用此方法筛选与深度关联不匹配的记录。例如,您可以找到没有被特定用户评论的文章。

$query = $articlesTable
    ->find()
    ->notMatching('Comments.Users', function ($q) {
        return $q->where(['username' => 'jose']);
    });

由于没有任何评论的文章也满足上述条件,因此您可能想要在同一个查询中结合 matching()notMatching()。以下示例将找到至少有一条评论但没有被特定用户评论的文章。

$query = $articlesTable
    ->find()
    ->notMatching('Comments.Users', function ($q) {
        return $q->where(['username' => 'jose']);
    })
    ->matching('Comments');

注意

由于 notMatching() 将创建一个 LEFT JOIN,因此您可能需要考虑在查找查询中调用 distinct,因为否则您可能会得到重复行。

请记住,与 matching() 函数相反,notMatching() 不会在结果中的 _matchingData 属性中添加任何数据。

使用 leftJoinWith

在某些情况下,您可能想要根据关联计算结果,而无需加载其所有记录。例如,如果您想加载一篇文章的评论总数以及所有文章数据,可以使用 leftJoinWith() 函数。

$query = $articlesTable->find();
$query->select(['total_comments' => $query->func()->count('Comments.id')])
    ->leftJoinWith('Comments')
    ->groupBy(['Articles.id'])
    ->enableAutoFields(true);

上面查询的结果将包含文章数据以及每个文章的 total_comments 属性。

leftJoinWith() 也可以与深度嵌套的关联一起使用。这对于例如获取每个作者被特定词标记的文章数量很有用。

$query = $authorsTable
    ->find()
    ->select(['total_articles' => $query->func()->count('Articles.id')])
    ->leftJoinWith('Articles.Tags', function ($q) {
        return $q->where(['Tags.name' => 'awesome']);
    })
    ->groupBy(['Authors.id'])
    ->enableAutoFields(true);

此函数不会将指定关联中的任何列加载到结果集中。

更改获取策略

如前所述,您可以在 contain() 中自定义关联使用的 strategy

如果您查看 BelongsToHasOne 关联 选项,默认的“join”策略和“INNER” joinType 可以更改为“select”。

$query = $articles->find()->contain([
    'Comments' => [
        'strategy' => 'select',
    ]
]);

当您需要添加在连接中不适用的条件时,这会很有用。这也使得查询在连接中不允许的表成为可能,例如单独的数据库。

通常,您在 Table::initialize() 中定义关联时设置关联的策略,但您可以手动永久更改策略。

$articles->Comments->setStrategy('select');

使用子查询策略获取

随着您的表大小的增长,从这些表中获取关联可能会变慢,尤其是在您一次查询大量批次时。使用 subquery 策略是优化 hasManybelongsToMany 关联加载的好方法。

$query = $articles->find()->contain([
    'Comments' => [
            'strategy' => 'subquery',
            'queryBuilder' => function ($q) {
                return $q->where(['Comments.approved' => true]);
            }
    ]
]);

结果将与使用默认策略相同,但这可以极大地提高某些数据库的查询和获取时间,特别是它允许在限制每个查询绑定参数数量的数据库中一次获取大量数据,例如 **Microsoft SQL Server**。

延迟加载关联

虽然 CakePHP 使用急切加载来获取您的关联,但在某些情况下,您可能需要延迟加载关联。有关更多信息,请参阅 延迟加载关联加载其他关联 部分。

使用结果集

一旦查询使用 all() 执行,您将获得一个 Cake\ORM\ResultSet 的实例。此对象提供了强大的方法来操作查询产生的数据。ResultSet 是一个 集合,您可以在 ResultSet 对象上使用任何集合方法。

结果集对象将从底层准备好的语句中延迟加载行。默认情况下,结果将在内存中进行缓冲,允许您多次迭代结果集,或缓存和迭代结果。

结果集允许您缓存/序列化或将结果编码为 JSON 以用于 API 结果。

// In a controller or table method.
$results = $query->all();

// Serialized
$serialized = serialize($results);

// Json
$json = json_encode($results);

序列化和 JSON 编码结果集都按预期工作。序列化数据可以反序列化为工作结果集。转换为 JSON 尊重结果集中所有实体对象的隐藏和虚拟字段设置。

结果集是一个“集合”对象,支持与 集合对象 相同的方法。例如,您可以通过运行以下代码来提取一组文章中唯一标签的列表。

// In a controller or table method.
$query = $articles->find()->contain(['Tags']);

$reducer = function ($output, $value) {
    if (!in_array($value, $output)) {
        $output[] = $value;
    }

    return $output;
};

$uniqueTags = $query->all()
    ->extract('tags.name')
    ->reduce($reducer, []);

与结果集一起使用集合方法的其他一些示例是

// Filter the rows by a calculated property
$filtered = $results->filter(function ($row) {
    return $row->is_recent;
});

// Create an associative array from result properties
$results = $articles->find()->contain(['Authors'])->all();

$authorList = $results->combine('id', 'author.name');

有关如何使用集合功能对结果集进行操作的更多详细信息,请参见Collections章节。 Adding Calculated Fields部分展示了如何添加计算字段或替换结果集。

从结果集中获取第一条和最后一条记录

可以使用first()last()方法从结果集中获取相应的记录。

$result = $articles->find('all')->all();

// Get the first and/or last result.
$row = $result->first();
$row = $result->last();

从结果集中获取任意索引

可以使用skip()first()从结果集中获取任意记录。

$result = $articles->find('all')->all();

// Get the 5th record
$row = $result->skip(4)->first();

检查结果集是否为空

可以使用结果集对象上的isEmpty()方法查看它是否包含任何行。

// Check results
$results = $query->all();
$results->isEmpty();

加载附加关联

创建结果集后,您可能需要加载附加关联。这是延迟急切加载数据的最佳时机。您可以使用loadInto()加载附加关联。

$articles = $this->Articles->find()->all();
$withMore = $this->Articles->loadInto($articles, ['Comments', 'Users']);

可以限制关联返回的数据并通过条件对其进行过滤。要指定条件,请传递一个匿名函数,该函数将查询对象(\Cake\ORM\Query)作为第一个参数接收。

$user = $this->Users->get($id);
$withMore = $this->Users->loadInto($user, ['Posts' => function (Query $query) {
    return $query->where(['Posts.status' => 'published']);
}]);

您可以将附加数据急切加载到单个实体或实体集合中。

使用 Map/Reduce 修改结果

通常,查找操作需要对数据库中找到的数据进行后处理。虽然实体的 getter 方法可以处理大多数虚拟字段生成或特殊数据格式,但有时您需要以更基本的方式更改数据结构。

对于这些情况,SelectQuery对象提供了mapReduce()方法,这是一种在从数据库中获取结果后处理结果的方式。

更改数据结构的常见示例是根据某些条件将结果分组在一起。对于此任务,我们可以使用mapReduce()函数。我们需要两个可调用函数,即$mapper$reducer$mapper可调用函数接收来自数据库的当前结果作为第一个参数,迭代键作为第二个参数,最后它接收正在运行的MapReduce例程的实例。

$mapper = function ($article, $key, $mapReduce) {
    $status = 'published';
    if ($article->isDraft() || $article->isInReview()) {
        $status = 'unpublished';
    }
    $mapReduce->emitIntermediate($article, $status);
};

在上面的示例中,$mapper正在计算文章的状态,无论是已发布还是未发布,然后它在MapReduce实例上调用emitIntermediate()。此方法将文章存储在标记为已发布或未发布的文章列表中。

map-reduce 过程的下一步是合并最终结果。对于在映射器中创建的每个状态,$reducer函数将被调用,以便您可以执行任何额外的处理。此函数将接收特定“桶”中的文章列表作为第一个参数,它需要处理的“桶”的名称作为第二个参数,以及在mapper()函数中,MapReduce例程的实例作为第三个参数。在我们的示例中,我们无需进行任何额外的处理,因此我们只emit()最终结果。

$reducer = function ($articles, $status, $mapReduce) {
    $mapReduce->emit($articles, $status);
};

最后,我们可以将这两个函数组合在一起进行分组。

$articlesByStatus = $articles->find()
    ->where(['author_id' => 1])
    ->mapReduce($mapper, $reducer)
    ->all();

foreach ($articlesByStatus as $status => $articles) {
    echo sprintf("There are %d %s articles", count($articles), $status);
}

以上将输出以下行。

There are 4 published articles
There are 5 unpublished articles

当然,这是一个简单的示例,实际上可以在没有 map-reduce 过程的帮助下以另一种方式解决。现在,让我们看一下另一个 reducer 函数需要做的事情比仅仅发出结果更多。

计算最常提到的词,其中文章包含关于 CakePHP 的信息,与往常一样,我们需要一个映射器函数。

$mapper = function ($article, $key, $mapReduce) {
    if (stripos($article['body'], 'cakephp') === false) {
        return;
    }

    $words = array_map('strtolower', explode(' ', $article['body']));
    foreach ($words as $word) {
        $mapReduce->emitIntermediate($article['id'], $word);
    }
};

它首先检查文章正文中是否存在“cakephp”一词,然后将正文分解成单个词。每个词都将创建自己的bucket,其中将存储每个文章 ID。现在让我们减少结果以仅提取计数。

$reducer = function ($occurrences, $word, $mapReduce) {
    $mapReduce->emit(count($occurrences), $word);
}

最后,我们将所有内容整合在一起。

$wordCount = $articles->find()
    ->where(['published' => true])
    ->andWhere(['published_date >=' => new DateTime('2014-01-01')])
    ->disableHydration()
    ->mapReduce($mapper, $reducer)
    ->all()
    ->toArray();

如果我们不清除停用词,这可能会返回一个非常大的数组,但它可能看起来像这样。

[
    'cakephp' => 100,
    'awesome' => 39,
    'impressive' => 57,
    'outstanding' => 10,
    'mind-blowing' => 83
]

最后一个例子,你将成为 map-reduce 专家。想象一下你有一个friends表,你想在我们的数据库中找到“假朋友”,或者更确切地说,是彼此不关注的人。让我们从我们的mapper()函数开始。

$mapper = function ($rel, $key, $mr) {
    $mr->emitIntermediate($rel['target_user_id'], $rel['source_user_id']);
    $mr->emitIntermediate(-$rel['source_user_id'], $rel['target_user_id']);
};

中间数组将如下所示。

[
    1 => [2, 3, 4, 5, -3, -5],
    2 => [-1],
    3 => [-1, 1, 6],
    4 => [-1],
    5 => [-1, 1],
    6 => [-3],
    ...
]

正数表示用户(用第一级键表示)正在关注他们,负数表示用户被他们关注。

现在是时候减少它了。对于对 reducer 的每次调用,它将接收每个用户的关注者列表。

$reducer = function ($friends, $user, $mr) {
    $fakeFriends = [];

    foreach ($friends as $friend) {
        if ($friend > 0 && !in_array(-$friend, $friends)) {
            $fakeFriends[] = $friend;
        }
    }

    if ($fakeFriends) {
        $mr->emit($fakeFriends, $user);
    }
};

我们向查询提供我们的函数。

$fakeFriends = $friends->find()
    ->disableHydration()
    ->mapReduce($mapper, $reducer)
    ->all()
    ->toArray();

这将返回一个类似于此的数组。

[
    1 => [2, 4],
    3 => [6]
    ...
]

生成的数组意味着,例如,ID 为1的用户关注用户24,但他们没有关注1

堆叠多个操作

在查询中使用mapReduce不会立即执行它。该操作将被注册为在尝试获取第一个结果时立即运行。这允许您在添加 map-reduce 例程后继续将其他方法和过滤器链接到查询。

$query = $articles->find()
    ->where(['published' => true])
    ->mapReduce($mapper, $reducer);

// At a later point in your app:
$query->where(['created >=' => new DateTime('1 day ago')]);

这对于构建自定义查找器方法特别有用,如Custom Finder Methods部分所述。

public function findPublished(SelectQuery $query)
{
    return $query->where(['published' => true]);
}

public function findRecent(SelectQuery $query)
{
    return $query->where(['created >=' => new DateTime('1 day ago')]);
}

public function findCommonWords(SelectQuery $query)
{
    // Same as in the common words example in the previous section
    $mapper = ...;
    $reducer = ...;

    return $query->mapReduce($mapper, $reducer);
}

$commonWords = $articles
    ->find('commonWords')
    ->find('published')
    ->find('recent');

此外,还可以为单个查询堆叠多个mapReduce操作。例如,如果我们想获得文章中最常用的词,但随后将其过滤以仅返回在所有文章中出现超过 20 次的词。

$mapper = function ($count, $word, $mr) {
    if ($count > 20) {
        $mr->emit($count, $word);
    }
};

$articles->find('commonWords')->mapReduce($mapper)->all();

删除所有堆叠的 Map-reduce 操作

在某些情况下,您可能希望修改SelectQuery对象,以便根本不执行任何mapReduce操作。这可以通过将两个参数都作为 null 调用该方法,并将第三个参数(覆盖)作为true来完成。

$query->mapReduce(null, null, true);