当无法使用@orderBy注释时,根据关联实体订购原则集合


Ordering Doctrine Collection based on associated Entity when it is not possible to use the @orderBy annotation

我想了解根据相关实体订购教义集合的最佳方式。在这种情况下,无法使用@orderBy注释。

我在互联网上找到了 5 种解决方案。

1)向AbstractEntity添加一个方法(根据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)创建一个Twig扩展,如果你只需要在模板中进行排序(根据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)将集合转换为数组,然后对其进行排序(根据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)使用ArrayIterator对集合进行排序(根据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)创建一个服务来收集有序集合,然后替换无序集合(我没有一个例子,但我认为很清楚)。我认为这是最丑陋的解决方案。

根据您的经验,哪个是最佳解决方案?您还有其他建议以更有效/优雅的方式订购系列吗?

谢谢。

前提

您提出了 5 个有效/体面的解决方案,但我认为所有解决方案都可以减少到两种情况,并有一些小的变体。

我们知道排序总是O(NlogN)的,所以所有的解在理论上都有相同的性能。但由于这是原则,SQL查询的数量和水化方法(即将数据从数组转换为对象实例)是瓶颈。

因此,您需要选择"最佳方法",具体取决于何时需要加载实体以及您将如何处理它们。

这些是我的"最佳解决方案",在一般情况下,我更喜欢我的解决方案 A)

A) 加载器/存储库服务中的 DQL

没有你的情况(不知何故有 5,请参阅最后的注释注释)。阿尔贝托·费尔南德斯在评论中为您指出了正确的方向。

最佳情况

DQL 是(可能)最快的方法,因为委托排序到 DBMS,它对此进行了高度优化。DQL 还全面控制要在单个查询中获取哪些实体以及冻结模式。

缺点

不可能

(AFAIK) 通过配置修改由 Doctrine 代理类生成的查询,因此您的应用程序需要使用存储库并在每次加载实体(或覆盖默认实体)时调用正确的方法。

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) 在 PHP 中预先加载和排序

与案例类似

案例2),3)和4)只是在不同的地方完成的相同事情。我的版本是一般情况,每当获取实体时都适用。如果您必须选择其中之一,那么我认为解决方案 3) 是最方便的,因为不要弄乱实体并且始终可用,但使用 EAGER 加载(继续阅读)。

最佳情况

如果始终读取关联的实体,但无法(或方便)添加服务,则所有实体都应以 EAGER-ly 方式加载。然后,只要对应用程序有意义,就可以由 PHP 完成排序:在事件侦听器中、在控制器中、在树枝模板中......如果应始终加载实体,则事件侦听器是最佳选择。

缺点

不如 DQL 灵活,当集合很大时,在 PHP 中进行排序可能是一个缓慢的操作。此外,实体需要作为 Object 进行水合,这很慢,如果集合不用于其他目的,则矫枉过正。当心延迟加载,因为这将为每个实体触发一个查询。

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>

主体.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);
        }
    }
}

结语

  • 我不会按原样使用案例 1),因为它非常复杂并引入了减少封装的继承。另外,我认为它具有与我的示例相同的复杂性和性能。
  • 案例5)不一定是坏事。如果"服务"是应用程序存储库,并且它使用 DQL 进行排序,那么这是我的第一个最佳情况。如果自定义服务仅用于对集合进行排序,那么我认为绝对不是一个好的解决方案。
  • 在这里编写的所有代码都没有准备好"复制粘贴",因为我的目标是展示我的观点。希望这将是一个很好的起点。

免責聲明

这些是"我的"最佳解决方案,正如我在作品中所做的那样。希望对您和其他人有所帮助。