php - Ordenar una colección de Doctrine basada en una entidad asociada cuando no es posible usar la anotación @orderBy
symfony doctrine-orm (1)
Premisa
Usted propuso 5 soluciones válidas / decentes, pero creo que todo podría reducirse a dos casos, con algunas variantes menores.
Sabemos que la clasificación siempre es O(NlogN)
, por lo que todas las soluciones tienen teóricamente el mismo rendimiento. Pero como se trata de Doctrine, la cantidad de consultas SQL y los métodos de hidratación (es decir, la conversión de datos de una matriz a una instancia de objeto) son los cuellos de botella.
Por lo tanto, debe elegir el "mejor método", dependiendo de cuándo necesita que se carguen las entidades y qué hará con ellas.
Estas son mis "mejores soluciones" y, en un caso general, prefiero mi solución A)
A) DQL en un servicio cargador / repositorio
Similar a
Ninguno de sus casos (de alguna manera con 5, vea las notas finales). Alberto Fernández te señaló en la dirección correcta en un comentario.
Mejor cuando
DQL es (potencialmente) el método más rápido, ya que la clasificación delegada a DBMS está altamente optimizada para esto. DQL también ofrece controles totales sobre qué entidades buscar en una sola consulta y el modo de hidratación.
Inconvenientes
No es posible (AFAIK) modificar la consulta generada por las clases de Proxy de Doctrine por la configuración, por lo que su aplicación necesita usar un Repositorio y llamar al método adecuado cada vez que cargue sus entidades (o invalide el predeterminado).
Ejemplo
class MainEntityRepository extends EntityRepository
{
public function findSorted(array $conditions)
{
$qb = $this->createQueryBuilder(''e'')
->innerJoin(''e.association'', ''a'')
->orderBy(''a.value'')
;
// if you always/frequently read ''a'' entities uncomment this to load EAGER-ly
// $qb->select(''e'', ''a'');
// If you just need data for display (e.g. in Twig only)
// return $qb->getQuery()->getResult(Query::HYDRATE_ARRAY);
return $qb->getQuery()->getResult();
}
}
B) Cargado e impaciente ordenando en PHP
Similar al caso
El caso 2), 3) y 4) son exactamente lo mismo que se hace en un lugar diferente. Mi versión es un caso general que se aplica cada vez que se buscan las entidades. Si tiene que elegir uno de estos, creo que la solución 3) es la más conveniente, ya que no se meta con la entidad y siempre está disponible, pero use la carga de EAGER (siga leyendo).
Mejor cuando
Si las entidades asociadas siempre se leen, pero no es posible (o conveniente) agregar un servicio, entonces todas las entidades deben cargarse EAGER-ly. PHP puede hacer la clasificación, siempre que tenga sentido para la aplicación: en un detector de eventos, en un controlador, en una plantilla de ramita ... Si las entidades deben cargarse siempre, entonces un detector de eventos es la mejor opción.
Inconvenientes
Menos flexible que DQL, y la clasificación en PHP puede ser una operación lenta cuando la colección es grande. Además, las entidades deben hidratarse como Objeto, que es lento, y es excesivo si la colección no se utiliza para otro propósito. Tenga cuidado con la carga perezosa, ya que esto activará una consulta para cada entidad.
Ejemplo
MainEntity.orm.xml:
<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping>
<entity name="MainEntity">
<id name="id" type="integer" />
<one-to-many field="collection" target-entity="LinkedEntity" fetch="EAGER" />
<entity-listeners>
<entity-listener class="MainEntityListener"/>
</entity-listeners>
</entity>
</doctrine-mapping>
MainEntity.php:
class MainEntityListener
{
private $id;
private $collection;
public function __construct()
{
$this->collection = new ArrayCollection();
}
// this works only with Doctrine 2.5+, in previous version association where not loaded on event
public function postLoad(array $conditions)
{
/*
* From your example 1)
* Remember that $this->collection is an ArryCollection when constructor is called,
* but a PersistentCollection when are loaded from DB. Don''t recreate the instance!
*/
// Get the values for the ArrayCollection and sort it using the function
$values = $this->collection->getValues();
// sort as you like
asort($values);
// Clear the current collection values and reintroduce in new order.
$collection->clear();
foreach ($values as $key => $item) {
$collection->set($key, $item);
}
}
}
Notas finales
- No usaré el caso 1) como es, ya que es muy complicado e introduce una herencia que reduce la encapsulación. Además, creo que tiene la misma complejidad y rendimiento de mi ejemplo.
- El caso 5) no es necesariamente malo. Si "el servicio" es el repositorio de la aplicación, y usa DQL para ordenar, entonces es mi primer mejor caso. Si es un servicio personalizado solo para ordenar una colección, creo que definitivamente no es una buena solución.
- Todos los códigos que escribí aquí no están listos para "copiar y pegar", ya que mi objetivo era mostrar mi punto de vista. Espero que sea un buen punto de partida.
Renuncia
Estas son "mis" mejores soluciones, como lo hago en mis trabajos. La esperanza te ayudará a ti y a los demás.
Me gustaría entender la mejor manera de ordenar una Colección Doctrine basada en una Entidad asociada. En este caso, no es posible utilizar la anotación @orderBy.
He encontrado 5 soluciones en internet.
1) Agregar un método a AbstractEntity (de acuerdo con Ian Belter https://stackoverflow.com/a/22183527/1148260 )
/**
* This method will change the order of elements within a Collection based on the given method.
* It preserves array keys to avoid any direct access issues but will order the elements
* within the array so that iteration will be done in the requested order.
*
* @param string $property
* @param array $calledMethods
*
* @return $this
* @throws /InvalidArgumentException
*/
public function orderCollection($property, $calledMethods = array())
{
/** @var Collection $collection */
$collection = $this->$property;
// If we have a PersistentCollection, make sure it is initialized, then unwrap it so we
// can edit the underlying ArrayCollection without firing the changed method on the
// PersistentCollection. We''re only going in and changing the order of the underlying ArrayCollection.
if ($collection instanceOf PersistentCollection) {
/** @var PersistentCollection $collection */
if (false === $collection->isInitialized()) {
$collection->initialize();
}
$collection = $collection->unwrap();
}
if (!$collection instanceOf ArrayCollection) {
throw new InvalidArgumentException(''First argument of orderCollection must reference a PersistentCollection|ArrayCollection within $this.'');
}
$uaSortFunction = function($first, $second) use ($calledMethods) {
// Loop through $calledMethods until we find a orderable difference
foreach ($calledMethods as $callMethod => $order) {
// If no order was set, swap k => v values and set ASC as default.
if (false == in_array($order, array(''ASC'', ''DESC'')) ) {
$callMethod = $order;
$order = ''ASC'';
}
if (true == is_string($first->$callMethod())) {
// String Compare
$result = strcasecmp($first->$callMethod(), $second->$callMethod());
} else {
// Numeric Compare
$difference = ($first->$callMethod() - $second->$callMethod());
// This will convert non-zero $results to 1 or -1 or zero values to 0
// i.e. -22/22 = -1; 0.4/0.4 = 1;
$result = (0 != $difference) ? $difference / abs($difference): 0;
}
// ''Reverse'' result if DESC given
if (''DESC'' == $order) {
$result *= -1;
}
// If we have a result, return it, else continue looping
if (0 !== (int) $result) {
return (int) $result;
}
}
// No result, return 0
return 0;
};
// Get the values for the ArrayCollection and sort it using the function
$values = $collection->getValues();
uasort($values, $uaSortFunction);
// Clear the current collection values and reintroduce in new order.
$collection->clear();
foreach ($values as $key => $item) {
$collection->set($key, $item);
}
return $this;
}
2) Crear una extensión Twig, si necesita la clasificación solo en una plantilla (de acuerdo con Kris https://stackoverflow.com/a/12505347/1148260 )
use Doctrine/Common/Collections/Collection;
public function sort(Collection $objects, $name, $property = null)
{
$values = $objects->getValues();
usort($values, function ($a, $b) use ($name, $property) {
$name = ''get'' . $name;
if ($property) {
$property = ''get'' . $property;
return strcasecmp($a->$name()->$property(), $b->$name()->$property());
} else {
return strcasecmp($a->$name(), $b->$name());
}
});
return $values;
}
3) Transformar la colección en una matriz y luego clasificarla (de acuerdo con Benjamin Eberlei https://groups.google.com/d/msg/doctrine-user/zCKG98dPiDY/oOSZBMabebwJ )
public function getSortedByFoo()
{
$arr = $this->arrayCollection->toArray();
usort($arr, function($a, $b) {
if ($a->getFoo() > $b->getFoo()) {
return -1;
}
//...
});
return $arr;
}
4) Usar ArrayIterator para ordenar la colección (de acuerdo con nifr https://stackoverflow.com/a/16707694/1148260 )
$iterator = $collection->getIterator();
$iterator->uasort(function ($a, $b) {
return ($a->getPropery() < $b->getProperty()) ? -1 : 1;
});
$collection = new ArrayCollection(iterator_to_array($iterator));
5) Crear un servicio para recopilar la colección ordenada y luego reemplazar la desordenada (no tengo un ejemplo pero creo que está bastante claro). Creo que esta es la solución más fea.
¿Cuál es la mejor solución según tu experiencia? ¿Tiene otras sugerencias para ordenar una colección de una manera más efectiva / elegante?
Muchas gracias.