query cake cakephp orm associations cakephp-3.0 query-builder

cakephp - cake - ¿Cómo limitar las asociaciones contenidas por registro/grupo?



query builder cakephp 3 (1)

Lo que está buscando es una solución al problema de mayor n-por-grupo . No mencionó ningún RDBMS específico, pero no obstante vea también http://dev.mysql.com/doc/refman/5.6/en/example-maximum-column-group-row.html

Así que demos una oportunidad, aquí hay tres opciones que se pueden aplicar a nivel de asociación, sin embargo, puede considerarlas como no "directas".

Para algo HasMany específico, ¡ HasMany hacia abajo!

Seleccionar estrategia - Usar una unión en una agrupación, subconsulta de valor máximo

$this->hasOne(''TopAbstracts'', [ ''className'' => ''Abstracts'', ''strategy'' => ''select'', ''conditions'' => function (/Cake/Database/Expression/QueryExpression $exp, /Cake/ORM/Query $query) { $query->innerJoin( [ ''AbstractsFilter'' => $query ->connection() ->newQuery() ->select([''article_id'', ''points'' => $query->func()->max(''points'')]) ->from(''abstracts'') ->group(''article_id'') ], [ ''TopAbstracts.article_id = AbstractsFilter.article_id'', ''TopAbstracts.points = AbstractsFilter.points'' ] ); return []; } ]);

Esto seleccionará los resúmenes superiores a través de una consulta de combinación que se basa en los puntos máximos, se verá algo así como

SELECT TopAbstracts.id AS `TopAbstracts__id`, ... FROM abstracts TopAbstracts INNER JOIN ( SELECT article_id, (MAX(points)) AS `points` FROM abstracts GROUP BY article_id ) FilterAbstracts ON ( TopAbstracts.article_id = FilterAbstracts.article_id AND TopAbstracts.points = FilterAbstracts.points ) WHERE TopAbstracts.article_id in (1,2,3,4,5,6,7,8, ...)

Seleccionar estrategia: usar el filtro de autocombinación izquierda

$this->hasOne(''TopAbstracts'', [ ''className'' => ''Abstracts'', ''strategy'' => ''select'', ''conditions'' => function (/Cake/Database/Expression/QueryExpression $exp, /Cake/ORM/Query $query) { $query->leftJoin( [''AbstractsFilter'' => ''abstracts''], [ ''TopAbstracts.article_id = AbstractsFilter.article_id'', ''TopAbstracts.points < AbstractsFilter.points'' ]); return $exp->add([''AbstractsFilter.id IS NULL'']); } ]);

Esto usará una autocombinación que se filtra según las filas que no tienen a.points < b.points , se verá algo así como

SELECT TopAbstracts.id AS `TopAbstracts__id`, ... FROM abstracts TopAbstracts LEFT JOIN abstracts FilterAbstracts ON ( TopAbstracts.article_id = FilterAbstracts.article_id AND TopAbstracts.points < FilterAbstracts.points ) WHERE (FilterAbstracts.id IS NULL AND TopAbstracts.article_id in (1,2,3,4,5,6,7,8, ...))

Unirse a la estrategia: utilizar una subconsulta para la condición de unión

$this->hasOne(''TopAbstracts'', [ ''className'' => ''Abstracts'', ''foreignKey'' => false, ''conditions'' => function (/Cake/Database/Expression/QueryExpression $exp, /Cake/ORM/Query $query) { $subquery = $query ->connection() ->newQuery() ->select([''SubTopAbstracts.id'']) ->from([''SubTopAbstracts'' => ''abstracts'']) ->where([''Articles.id = SubTopAbstracts.article_id'']) ->order([''SubTopAbstracts.points'' => ''DESC'']) ->limit(1); return $exp->add([''TopAbstracts.id'' => $subquery]); } ]);

Esto utilizará una subconsulta correlacionada que usa una selección bastante específica con un orden y una limitación simples para elegir el comentario principal. Tenga en cuenta que la opción foreignKey se establece en false para evitar que una condición adicional Articles.id = TopAbstracts.article_id se compile en las condiciones de unión.

La consulta se verá algo así como

SELECT Articles.id AS `Articles__id`, ... , TopAbstracts.id AS `TopAbstracts__id`, ... FROM articles Articles LEFT JOIN abstracts TopAbstracts ON ( TopAbstracts.id = ( SELECT SubTopAbstracts.id FROM abstracts SubTopAbstracts WHERE Articles.id = SubTopAbstracts.article_id ORDER BY SubTopAbstracts.points DESC LIMIT 1 ) )

Todas estas 3 opciones consultarán e inyectarán los registros sin ningún hacker, simplemente no es muy "directo".

Un enfoque manual

En aras de la exhaustividad, por supuesto, siempre es posible cargar manualmente los registros asociados y formatear los resultados de forma adecuada, por ejemplo, utilizando formateadores de resultados, consulte por ejemplo, la entidad CakePHP contiene sin clave externa.

Seleccionar estrategia y orden invertido

Solo como referencia, una de las soluciones raras con la que tropecé inicialmente. ¡Este realmente no debería ser usado!

Esto seleccionará todos los resúmenes asociados, y luego el ORM iterará sobre ellos y para cada artículo elegirá el primero con un valor de article_id coincidente. Entonces, en teoría, cuando se ordena el trazado de points , el ORM debe elegir el que tiene más puntos.

Si bien esperaba que esto funcionara de la caja, parece que el ORM itera sobre los resultados en orden inverso, lo que hará que se elijan las filas incorrectas. Para que esto funcione, la consulta debe usar el orden opuesto que normalmente debería usarse, es decir, ASC lugar de DESC .

$this->hasOne(''TopAbstracts'', [ ''className'' => ''Abstracts'', ''foreignKey'' => ''abstract_id'', ''strategy'' => ''select'', ''conditions'' => function (/Cake/Database/Expression/QueryExpression $exp, /Cake/ORM/Query $query) { $query->order([''TopAbstracts.points'' => ''ASC'']); return []; } ]);

Además, la función debe devolver una matriz vacía en lugar de la expresión como se muestra en la respuesta vinculada, ya que esto provocará que se compile SQL no válido. Ambos comportamientos, la iteración de orden inverso y el SQL no válido pueden ser errores.

Si bien esto funcionará, siempre seleccionará todos los resúmenes asociados, no solo los más importantes, que podrían considerarse bastante ineficientes, y se vería algo así como

SELECT Articles.id AS `Articles__id`, ... FROM articles Articles

SELECT TopAbstracts.id AS `TopAbstracts__id`, ... FROM abstracts TopAbstracts WHERE TopAbstracts.article_id in (1,2,3,4,5,6,7,8, ...) ORDER BY TopAbstracts.points ASC

HasMany asociaciones

Le di una HasMany asociaciones HasMany , pero ahora estoy demasiado ocupado como para seguir adelante con esto ... simplemente ROW_NUMBER() una asociación personalizada específica de MySQL para fines de prueba, basada en la emulación ROW_NUMBER() similar a MySQL seleccione los mejores registros X para cada individuo en la mesa

Si alguien está interesado, consulte https://gist.github.com/ndm2/039da4009df1c5bf1c262583603f8298

Tengo un modelo, artículos, que tiene muchos resúmenes. Quiero cargar los 10 últimos artículos, y para cada artículo, el resumen con el mayor número de puntos. Mi función se ve así:

public function getArticles($category, $viewName) { $subArticles = $this->Articles->findByCategory($category)->contain([ ''Abstracts'' => function ($q) { return $q ->select([''body'', ''points'', ''article_id'']) ->where([''Abstracts.approved'' => true]) ->limit(10) ->order([''Abstracts.points'' => ''DESC'']); } ]) ->limit(10) ->order([''Articles.created'' => ''DESC'']) ; $this->set( $viewName . ''Articles'', $subArticles ); }

El resultado que obtengo no es lo que intento sin embargo. Mirando a través del SQL, primero CakePHP está obteniendo los artículos.id de todo en la categoría (bien). Luego, CakePHP entra en la tabla de resúmenes, utilizando los 10 artículos que acaba de encontrar, y solicita los 10 resúmenes con los votos más altos (que pertenecen a esos artículos).

El problema es que quiero 1 resumen para cada artículo, no los 10 resúmenes que pertenecen a ningún artículo en esa categoría. ¿Cómo puedo arreglar esto? ¡Gracias!

EDITAR

ndm sugirió que esto era un duplicado de Usar límite () en el modelo contenido, así que intenté la solución allí. A saber, agregué esto a mi Modelo:

$this->hasOne(''TopAbstract'', [ ''className'' => ''Abstracts'', ''foreignKey'' => ''abstract_id'', ''strategy'' => ''select'', ''sort'' => [''TopAbstract.points'' => ''DESC''], ''conditions'' => function ($e, $query) { $query->limit(1); return $e; } ]);

Y luego trato de encontrar los Artículos por Categoría, con contener ([''TopAbstract'']), solo esto mata mi SQL. Muere una muerte horrible:

Error: SQLSTATE[HY000]: General error: 1 near ")": syntax error

¿Depurar ni siquiera muestra la consulta que lo mató, por lo que no estoy seguro de cómo depurar este?

EDITAR

Hablando un poco conmigo mismo, pero el error está definitivamente en la parte de ''condiciones'' de hasOne. Lo elimino y funciona bien. No se puede encontrar un ejemplo de cómo se supone que debe verse en las interwebs ... ¿alguien tiene alguna idea?