当不可能使用@orderBy注释时,基于关联实体的订购原则集合 [英] Ordering Doctrine Collection based on associated Entity when it is not possible to use the @orderBy annotation

查看:125
本文介绍了当不可能使用@orderBy注释时,基于关联实体的订购原则集合的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想了解基于相关实体订购教义收集的最佳方法。在这种情况下,不可能使用@orderBy注释。



我已经在互联网上找到了5个解决方案。



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

  / ** 
*此方法将根据给定的方法。
*它保留数组键以避免任何直接访问问题,但将在数组中排列元素
*,以便迭代将按要求的顺序完成。
*
* @param string $ property
* @param array $ calledMethods
*
* @return $ this
* @throws \InvalidArgumentException
* /
public function orderCollection($ property,$ calledMethods = array())
{
/ ** @var Collection $ collection * /
$ collection = $这 - > $财产;

//如果我们有一个PersistentCollection,请确保它被初始化,然后打开它,所以我们
//可以编辑底层的ArrayCollection,而不会在
/ / PersistentCollection我们只会进入并改变底层ArrayCollection的顺序。
if($ collection instanceOf PersistentCollection){
/ ** @var PersistentCollection $ collection * /
if(false === $ collection-> isInitialized()){
$收藏 - >初始化();
}
$ collection = $ collection-> unwrap();
}

if(!$ collection instanceOf ArrayCollection){
throw new InvalidArgumentException('orderCollection的第一个参数必须引用$ this中的PersistentCollection | ArrayCollection)。
}

$ uaSortFunction = function($ first,$ second)use($ calledMethods){

//循环通过$ calledMethods,直到找到有序的差异
foreach($ calledMethods as $ callMethod => $ order){

//如果没有设置顺序,则交换k => v值并将ASC设置为默认值。
if(false == in_array($ order,array('ASC','DESC'))){
$ callMethod = $ order;
$ order ='ASC';
}

if(true == is_string($ first-> $ callMethod())){

// String比较
$ result = strcasecmp($ first-> $ callMethod(),$ second-> $ callMethod());

} else {

//数值比较
$差异=($ first-> $ callMethod() - $ second-> $ callMethod() );
//这将将非零$结果转换为1或-1或零值为0
//即-22/22 = -1; 0.4 / 0.4 = 1;
$ result =(0!= $ difference)? $ difference / abs($ difference):0;
}

//如果DESC给定
,则'反向'结果if('DESC'== $ order){
$ result * = -1;
}

//如果我们有一个结果返回它,否则继续循环
if(0!==(int)$ result){
return int)$ result;
}
}

//没有结果,返回0
返回0;
};

//获取ArrayCollection的值并使用函数
$ values = $ collection-> getValues()进行排序;
uasort($ values,$ uaSortFunction);

//清除当前的集合值,并以新的顺序重新引入。
$ collection-> clear();
foreach($ values as $ key => $ item){
$ collection-> set($ key,$ item);
}

return $ this;
}

2)创建一个Twig扩展,如果您需要在模板中排序(根据Kris https://stackoverflow.com/a/12505347/1148260

 使用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());
}
});
返回$值;
}

3)将集合转换为数组,然后排序(根据本杰明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) code>,所以所有的解决方案在理论上都是一样的。但是由于这是Doctrine,SQL查询的数量和Hydration方法(即将数据从数组转换为对象实例)是瓶颈。



所以你需要选择最佳方法,具体取决于何时需要加载的实体以及您将如何处理这些实体。



这些是我的最佳解决方案一般情况我喜欢我的解决方案A)



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



/ h2>

没有你的情况(不知何故,5,看最后的笔记)。 AlbertoFernández在评论中指出了正确的方向。



最好当



DQL是(潜在的)最快的方法,因为委托排序到DBMS,这是高度优化的。 DQL还提供了在单个查询和水化模式中获取哪些实体的总体控制。



缺点



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



示例



  class MainEntityRepository扩展EntityRepository 
{
public function findSorted(array $ conditions)
{
$ qb = $ this-> createQueryBuilder('e')
- > innerJoin('e.association','a')
- > orderBy('a.value')
;
//如果你总是/经常阅读'a'实体取消注释加载EAGER-ly
// $ qb-> select('e','a');

//如果你只需要显示数据(例如在Twig中)
// return $ qb-> getQuery() - > getResult(Query :: HYDRATE_ARRAY);

return $ qb-> getQuery() - > getResult();
}
}



B)热切的加载和排序PHP < h1>

类似于案例



案例2),3)和4)在不同的地方完成相同的事情。我的版本是一般的情况,适用于实体被抓取。如果您必须选择其中之一,那么我认为解决方案3)是最方便的,因为不要混淆实体,并且始终可用,而是使用EAGER加载(阅读)。



最好当



如果相关联的实体总是被读取,但不可能(或方便)添加一个服务,那么所有实体应加载EAGER-ly。然后可以通过PHP完成排序,只要应用程序有意义:在事件侦听器中,在控制器中,在twig模板中...如果实体应该总是加载,则事件侦听器是最佳选项。 / p>

缺点



比DQL灵活,而在PHP中排序可能是一个缓慢的操作,当集合很大时。此外,实体需要被水化为对象,这是缓慢的,如果收集不用于其他目的,则是过度的。小心懒加载,因为这将为每个实体触发一个查询。



示例



MainEntity.orm .xml:

 <?xml version =1.0encoding =utf-8?> 
< doctrine-mapping>
< entity name =MainEntity>
< id name =idtype =integer/>
< one-to-many field =collectiontarget-entity =LinkedEntityfetch =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();
}

//这只适用于Doctrine 2.5+,在以前的版本关联中,没有加载在事件
public function postLoad(array $ conditions)
{
/ *
*从你的例子1)
*记住,$ this->集合是一个ArryCollection,当构造函数调用时,
*,而从DB加载一个PersistentCollection。不要重新创建实例!
* /

//获取ArrayCollection的值,并使用函数
$ values = $ this-> collection-> getValues();

//按照你喜欢的方式排序
asort($ values);

//清除当前的集合值,并以新的顺序重新引入。
$ collection-> clear();
foreach($ values as $ key => $ item){
$ collection-> set($ key,$ item);
}
}
}



Final Notes




  • 我不会使用case 1),因为非常复杂,并引入了减少封装的继承。此外,我认为我的例子有相同的复杂性和性能。

  • 案例5)不一定是坏的。如果服务是应用程序存储库,并且使用DQL进行排序,那么是我的第一个最好的例子。如果是一个定制服务,只能对一个集合进行排序,那么我认为绝对不是一个很好的解决方案。

  • 我在这里写的所有代码没有准备好复制粘贴,因为我目的是表明我的观点。希望这是一个很好的起点。



免责声明



我的最好的解决方案,就像我在作品中所做的一样。希望会帮助你和他人。


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.

I have found 5 solutions on the Internet.

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) Creating a Twig extension, if you need the sorting just in a template (according to 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) 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) 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) 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?

Thank you very much.

解决方案

Premise

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

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.

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

A) DQL in a loader/repository service

Similar to

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

Best when

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.

Drawbacks

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).

Example

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) 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).

Best when

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.

Drawbacks

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.

Example

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

Final Notes

  • I won't use case 1) as is, since is very complicated and introduce inheritance which reduce encapsulation. Also, I think that it has the same complexity and performance of my example.
  • Case 5) is not necessarily bad. If "the service" is the application repository, and it use DQL to sort, then is my first best case. If is a custom service only to sort a collection, then I think is definitely not a good solution.
  • All the codes I wrote here is not ready for "copy-paste", since my objective was to show my point of view. Hope it would be a good starting point.

Disclaimer

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

这篇关于当不可能使用@orderBy注释时,基于关联实体的订购原则集合的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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