Ассоциация
A hasMany
всегда будет использовать отдельный отдельный запрос, никогда несколько отдельных запросов.Разница между стратегиями select
и subquery
заключается в том, что одна будет напрямую сравниваться с массивом первичных ключей, а другая - с объединенным подзапросом, который будет соответствовать выбранным родительским записям.
То, что вы пытаетесьсостоит в том, чтобы выбрать great-n-per-group , что невозможно со встроенными загрузчиками ассоциаций, и это может быть немного сложнее в зависимости от используемой СУБД, проверьте, например, Как ограничить содержащиеся ассоциации для каждой записи / группы? для примера для MySQL <8.x с использованием настраиваемой ассоциации и загрузчика. </p>
Для СУБД, которые его поддерживают, просмотритеоконные функции.Вот пример загрузчика, который использует встроенные оконные функции, можно просто заменить его в связанном примере с ним, но имейте в виду, что он на самом деле не тестировался или что-то в этом роде, я просто отложил его в некоторых экспериментах:
namespace App\ORM\Association\Loader;
use Cake\Database\Expression\OrderByExpression;
use Cake\ORM\Association\Loader\SelectLoader;
class GroupLimitedSelectLoader extends SelectLoader
{
/**
* The group limit.
*
* @var int
*/
protected $limit;
/**
* The target table.
*
* @var \Cake\ORM\Table
*/
protected $target;
/**
* {@inheritdoc}
*/
public function __construct(array $options)
{
parent::__construct($options);
$this->limit = $options['limit'];
$this->target = $options['target'];
}
/**
* {@inheritdoc}
*/
protected function _defaultOptions()
{
return parent::_defaultOptions() + [
'limit' => $this->limit,
];
}
/**
* {@inheritdoc}
*/
protected function _buildQuery($options)
{
$key = $this->_linkField($options);
$keys = (array)$key;
$filter = $options['keys'];
$finder = $this->finder;
if (!isset($options['fields'])) {
$options['fields'] = [];
}
/* @var \Cake\ORM\Query $query */
$query = $finder();
if (isset($options['finder'])) {
list($finderName, $opts) = $this->_extractFinder($options['finder']);
$query = $query->find($finderName, $opts);
}
$rowNumberParts = ['ROW_NUMBER() OVER (PARTITION BY'];
for ($i = 0; $i < count($keys); $i ++) {
$rowNumberParts[] = $query->identifier($keys[$i]);
if ($i < count($keys) - 1) {
$rowNumberParts[] = ',';
}
}
$rowNumberParts[] = new OrderByExpression($options['sort']);
$rowNumberParts[] = ')';
$rowNumberField = $query
->newExpr()
->add($rowNumberParts)
->setConjunction('');
$rowNumberSubQuery = $this->target
->query()
->select(['__row_number' => $rowNumberField])
->where($options['conditions']);
$columns = $this->target->getSchema()->columns();
$rowNumberSubQuery->select(array_combine($columns, $columns));
$rowNumberSubQuery = $this->_addFilteringCondition($rowNumberSubQuery, $key, $filter);
$fetchQuery = $query
->select($options['fields'])
->from([$this->targetAlias => $rowNumberSubQuery])
->where([$this->targetAlias . '.__row_number <=' => $options['limit']])
->eagerLoaded(true)
->enableHydration($options['query']->isHydrationEnabled());
if (!empty($options['contain'])) {
$fetchQuery->contain($options['contain']);
}
if (!empty($options['queryBuilder'])) {
$fetchQuery = $options['queryBuilder']($fetchQuery);
}
$this->_assertFieldsPresent($fetchQuery, $keys);
return $fetchQuery;
}
}