【问题标题】:How can I order by NULL in DQL?如何在 DQL 中按 NULL 排序?
【发布时间】:2012-09-21 01:09:41
【问题描述】:

我正在使用 Symfony2 框架和 Doctrine ORM 构建一个应用程序。我有一张缺少一些 IATA 代码的航空公司的表格。我正在输出一个按此 IATA 代码排序的列表,但我得到了不希望的结果,即具有空 IATA 代码的记录排在顶部。

在 MySQL 中,这很简单,使用 ORDER BY ISNULL(code_iata), code_iata,但我不知道 DQL 的等价物是什么。我试过了

$er->createQueryBuilder('airline')->orderBy('ISNULL(airline.codeIata), airline.codeIata', 'ASC')

但这给了我一个语法错误。

Doctrine 文档也没有给我答案。有什么办法吗?

【问题讨论】:

    标签: symfony doctrine-orm dql


    【解决方案1】:

    您可以在 DQL 中使用以下技巧对 NULL 值进行最后排序

    $em->createQuery("SELECT c, -c.weight AS HIDDEN inverseWeight FROM Entity\Car c ORDER BY inverseWeight DESC");
    

    HIDDEN 关键字(自 Doctrine 2.2 起可用)将导致从结果集中省略 inverseWeight 字段,从而防止不受欢迎的 mixed results

    (排序字段的值是倒置的,因此顺序也必须倒置,这就是为什么查询使用DESC order,而不是ASC。)

    学分属于this answer

    【讨论】:

      【解决方案2】:

      最不显眼的通用解决方案是将CASE 表达式与HIDDEN 关键字结合使用。

         SELECT e,
           CASE WHEN e.field IS NULL THEN 1 ELSE 0 END HIDDEN _isFieldNull
           FROM FooBundle:Entity e
       ORDER BY _isFieldNull ASC
      

      适用于数字和其他字段类型,不需要扩展 Doctrine。

      【讨论】:

      • 这很完美!
      【解决方案3】:

      如果您想在 SQL 中执行类似于“NULLS LAST”的操作(在我的情况下使用 PostgreSQL):

      ORDER BY freq DESC NULLS LAST
      

      您可以将 COALESCE 函数与 Doctrine Query Builder 一起使用 (HIDDEN 将隐藏查询结果集中的“freq”字段)。

      $qb = $this->createQueryBuilder('d')
                 ->addSelect('COALESCE(d.freq, 0) AS HIDDEN freq')
                 ->orderBy('freq', 'DESC')
                 ->setMaxResults(20);
      

      【讨论】:

      • 谢谢托马斯,这拯救了我的一天。这里如何使用日期排序:->addSelect('COALESCE(news.date, \'0001-01-01\') AS HIDDEN date')->orderBy('date', 'DESC')
      【解决方案4】:

      这是一个自定义walker 的示例,可以准确获取您想要的内容。我从 Doctrine 的 github 问题中获取了它:

      https://github.com/doctrine/doctrine2/pull/100

      但是那里的代码在 MySQL 中对我不起作用。我已经修改它以在 MySQL 中工作,但我根本没有测试其他引擎。

      将以下walker类例如放在YourNS\Doctrine\Waler\目录中;

      <?php
      
      namespace YourNS\Doctrine\Walker;
      
      use Doctrine\ORM\Query\SqlWalker;
      
      class SortableNullsWalker extends SqlWalker
      {
         const NULLS_FIRST = 'NULLS FIRST';
         const NULLS_LAST = 'NULLS LAST';
      
         public function walkOrderByClause($orderByClause)
         {
            $sql = parent::walkOrderByClause($orderByClause);
      
            if ($nullFields = $this->getQuery()->getHint('SortableNullsWalker.fields'))
            {
               if (is_array($nullFields))
               {
                  $platform = $this->getConnection()->getDatabasePlatform()->getName();
                  switch ($platform)
                  {
                  case 'mysql':
                     // for mysql the nulls last is represented with - before the field name
                     foreach ($nullFields as $field => $sorting)
                     {
                        /**
                         * NULLs are considered lower than any non-NULL value,
                         * except if a – (minus) character is added before
                         * the column name and ASC is changed to DESC, or DESC to ASC;
                         * this minus-before-column-name feature seems undocumented.
                         */
                        if ('NULLS LAST' === $sorting)
                        {
                           $sql = preg_replace_callback('/ORDER BY (.+)'.'('.$field.') (ASC|DESC)/i', function($matches) {
                              if ($matches[3] === 'ASC') {
                                 $order = 'DESC';
                              } elseif ($matches[3] === 'DESC') {
                                 $order = 'ASC';
                              }
                              return ('ORDER BY -'.$matches[1].$matches[2].' '.$order);
                           }, $sql);
                        }
                     }
                        break;
                  case 'oracle':
                  case 'postgresql':
                     foreach ($nullFields as $field => $sorting)
                     {
                        $sql = preg_replace('/(\.' . $field . ') (ASC|DESC)?\s*/i', "$1 $2 " . $sorting, $sql);
                     }
                     break;
                  default:
                     // I don't know for other supported platforms.
                     break;
                     }
                  }
               }
      
               return $sql;
         }
      }
      

      然后:

      use YourNS\Doctrine\Walker\SortableNullsWalker;
      use Doctrine\ORM\Query;
      
      [...]
      
      $qb = $em->getRepository('YourNS:YourEntity')->createQueryBuilder('e');
      $qb
         ->orderBy('e.orderField')
         ;
      
      $entities = $qb->getQuery()
        ->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER,  '\YourNS\Doctrine\Walker\SortableNullsWalker')
        ->setHint('SortableNullsWalker.fields', array(
           'sortOrder' => SortableNullsWalker::NULLS_LAST
        ))
        ->getResult();
      

      【讨论】:

        【解决方案5】:

        【讨论】:

        • 谢谢,这似乎是最明智的做法,尽管相当复杂。我看到了有关创建自定义函数类的足够说明,但是如何在 QueryBuilder 中导入和实现呢?我已将它添加到我的全局 config.yml 中,然后我假设我不再需要在每个 FormType 类中导入它。但它是如何在 createQueryBuilder 调用中使用的呢?
        • 我在我的 ORM 配置中定义了 isnull_string 的自定义函数,因此我实现了它:$er-&gt;createQueryBuilder('airline')-&gt;orderBy('isnull_string(airline.codeIata), airline.codeIata', 'ASC'); 仍然给我一个语法错误。显然 CreateQueryBuilder 不像 CreateQuery 那样支持完整的 DQL 语法。很遗憾,因为我认为您不能在 FormBuilderInterface 中使用 createQuery(毕竟该选项被明确称为 query_builder)。
        【解决方案6】:

        默认情况下,MySQL 仍然会对NULL 值进行排序;如果排序为ASC,它将仅将其放在结果集的开头,如果排序为DESC,则将其放在末尾。在这里,您希望对 ASC 进行排序,但您希望 NULL 值位于底部。

        不幸的是,尽管 Doctrine 功能强大,但这里不会提供太多支持,因为功能支持有限,而且大部分仅限于 SELECTWHEREHAVING 子句。如果 QueryBuilder 满足以下任一条件,您实际上根本不会遇到问题:

        • select() 接受 ISNULL()
        • orderBy()addOrderBy() 支持 ISNULL()
        • 该类支持UNIONs 的概念(这样,您可以运行两个查询:一个codeIataNULL,另一个不是,您可以分别对每个查询进行排序)

        也就是说,您可以使用 ArtWorkAD 已经提到的用户定义函数,或者您可以使用两个不同的 Doctrine 查询复制最后一点:

        $airlinesWithCode = $er->createQueryBuilder("airline")
            ->where("airline.iataCode IS NULL")
            ->getQuery()
            ->getResult();
        $airlinesWithoutCode = $er->createQueryBuilder("airline")
            ->where("airline.iataCode IS NOT NULL")
            ->getQuery()
            ->getResult();
        

        然后你可以将它们组合成一个数组,或者在你的模板中独立处理它们。

        另一个想法是让 DQL 在一个数据集中返回所有内容,让 PHP 完成繁重的工作。比如:

        $airlines = $er->findAll();
        $sortedAirlines = array();
        // Add non-NULL values to the end if the sorted array
        foreach ($airlines as $airline)
            if ($airline->getCodeIata())
                $sortedAirlines[] = $airline;
        // Add NULL values to the end of the sorted array
        foreach ($airlines as $airline)
            if (!$airline->getCodeIata())
                $sortedAirlines[] = $airline;
        

        这两种方法的缺点是您将无法在 MySQL 中执行 LIMITs,因此它可能只适用于相对较小的数据集。

        无论如何,希望这能让你顺利上路!

        【讨论】:

          猜你喜欢
          • 2016-01-31
          • 2023-03-09
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2023-03-05
          • 1970-01-01
          • 2012-09-20
          • 1970-01-01
          相关资源
          最近更新 更多