当无法使用 @orderBy 注释时,根据关联实体对 Doctrine 集合进行排序 [英] Ordering Doctrine Collection based on associated Entity when it is not possible to use the @orderBy annotation

查看:17
本文介绍了当无法使用 @orderBy 注释时,根据关联实体对 Doctrine 集合进行排序的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想了解基于关联实体订购 Doctrine Collection 的最佳方式.在这种情况下,无法使用 @orderBy 注释.

I would like to understand the best way to order a Doctrine Collection based on associated Entity. In this case, it is not possible to use the @orderBy annotation.

我在网上找到了 5 个解决方案.

I have found 5 solutions on the Internet.

1) 向 AbstractEntity 添加方法(根据 Ian Belter https://stackoverflow.com/a/22183527/1148260)

1) Adding a method to the AbstractEntity (according to 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)

2) Creating a Twig extension, if you need the sorting just in a template (according to Kris https://stackoverflow.com/a/12505347/1148260)

use DoctrineCommonCollectionsCollection;

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)

3) Transforming the collection into an array and then sorting it (according to 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)

4) Using ArrayIterator to sort the collection (according to 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) Creating a service to gather the ordered collection and then replace the unordered one (I have not an example but I think it is pretty clear). I think this is the ugliest solution.

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

Which is the best solution according to you experience? Do you have other suggestions to order a collection in a more effective/elegant way?

非常感谢.

推荐答案

前提

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

Premise

You proposed 5 valid/decent solutions, but I think that all could be reduced down to two cases, with some minor variants.

我们知道排序总是O(NlogN),因此所有解决方案理论上具有相同的性能.但由于这是 Doctrine,SQL 查询的数量和 Hydration 方法(即将数据从数组转换为对象实例)是瓶颈.

We know that sorting is always O(NlogN), so all solution have theoretically the same performance. But since this is Doctrine, the number of SQL queries and the Hydration methods (i.e. converting data from array to object instance) are the bottlenecks.

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

So you need to choose the "best method", depending on when you need the entities to be loaded and what you'll do with them.

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

These are my "best solutions", and in a general case I prefer my solution A)

没有你的情况(不知何故有 5,见最后的注释).Alberto Fernández 在评论中为您指明了正确的方向.

None of your case (somehow with 5, see the final notes note). Alberto Fernández pointed you in the right direction in a comment.

DQL 是(可能)最快的方法,因为将排序委托给为此高度优化的 DBMS.DQL 还提供了对在单个查询中获取哪些实体和 hydrations 模式的总体控制.

DQL is (potentially) the fastest method, since delegate sorting to DBMS which is highly optimized for this. DQL also gives total controls on which entities to fetch in a single query and the hydrations mode.

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

示例

It is not possible (AFAIK) to modify query generated by Doctrine Proxy classes by configuration, so your application need to use a Repository and call the proper method every time you load your entities (or override the default one).

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 加载(请继续阅读).

B) Eager loading, and sorting in PHP

Similar to case

Case 2), 3) and 4) are just the same thing done in different place. My version is a general case which apply whenever the entities are fetched. If you have to choose one of these, then I think that solution 3) is the most convenient, since don't mess with the entity and is always available, but use EAGER loading (read on).

如果关联的实体总是被读取,但添加服务是不可能的(或不方便的),那么所有实体都应该立即加载.排序可以由 PHP 完成,只要它对应用程序有意义:在事件侦听器中,在控制器中,在树枝模板中......如果应该始终加载实体,那么事件侦听器是最好的选择.

If the the associated entities are always read, but it is not possible (or convenient) to add a service, then all entities should loaded EAGER-ly. Sorting then can be done by PHP, whenever it makes sense for the application: in an event listener, in a controller, in a twig template... If the entities should be always loaded, then an event listener is the best option.

不如DQL灵活,而且在PHP中排序在集合很大的时候可能是一个很慢的操作.此外,实体需要被水合为对象,这很慢,如果集合不用于其他目的,那就是矫枉过正.小心延迟加载,因为这会为每个实体触发一个查询.

Less flexible than DQL, and sorting in PHP may be a slow operation when the collection is big. Also, the entities need to be hydrated as Object which is slow, and is overkill if the collection is not used for other purpose. Beware of lazy-loading, since this will trigger one query for every entity.

MainEntity.orm.xml:

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:

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);
        }
    }
}

最后的笔记

  • 我不会照原样使用案例 1),因为它非常复杂并且引入了减少封装的继承.此外,我认为它与我的示例具有相同的复杂性和性能.
  • 情况 5) 不一定是坏事.如果服务"是应用程序存储库,并且它使用 DQL 进行排序,那么这是我的第一个最佳案例.如果是定制服务,只对一个集合进行排序,那么我认为绝对不是一个好的解决方案.
  • 我在这里写的所有代码都没有准备好复制粘贴",因为我的目的是表明我的观点.希望这是一个好的起点.
  • 这些是我的"最佳解决方案,正如我在我的作品中所做的那样.希望能帮到你和其他人.

    These are "my" best solutions, as I do it in my works. Hope will help you and others.

    这篇关于当无法使用 @orderBy 注释时,根据关联实体对 Doctrine 集合进行排序的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆