0

I'm finding it almost impossible to achieve my desired result, after so many attempts with different solutions (sub query, raw query, etc) found here and on other websites, I must ask this question.

My goal is to extract/get each "projects" rank based on their "score".

Consider "score" as int and with values like 1,2,6,4,8,10,200, etc.

The rank would be like this:

Rank - Score

  1. 200
  2. 10
  3. 8
  4. 6

For my question to be as simple and clear as possible, I renamed my actual tables/entities as below:

MainEntity (main_table):

/**
* @ORM\Id
* @ORM\Column(name="id")
* @ORM\GeneratedValue
*/
protected $id;

// other fields, un-related to this question

/**
* @ORM\OneToOne(targetEntity="Application\Entity\SecondTable", mappedBy="second_table_data")
*/
protected $second_table;

/**
* @ORM\OneToOne(targetEntity="Application\Entity\ThirdTable", mappedBy="third_table_data")
* 
*/
protected $third_table;

SecondEntity (second_table):

/**
* @ORM\Id
* @ORM\Column(name="id")
* @ORM\GeneratedValue
*/
protected $id;

// other fields, un-related to this question

/**
* @ORM\OneToOne(targetEntity="Application\Entity\SecondTable", inversedBy="second_table")
* @ORM\JoinColumn(name="project_id", referencedColumnName="id")
*/
private $second_table_data;

ThirdEntity (third_table):

/**
* @ORM\Id
* @ORM\Column(name="id")
* @ORM\GeneratedValue
*/
protected $id;

// other fields, un-related to this question

/** 
 * @ORM\Column(name="score")  
 */
protected $score;

/**
* @ORM\OneToOne(targetEntity="Application\Entity\ThirdTable", inversedBy="third_table")
* @ORM\JoinColumn(name="project_id", referencedColumnName="id")
*/
private $third_table_data;

And the repository function to select "all projects" ordered by their score:

public function findAllProjects()
{
    $entityManager = $this->getEntityManager();

    $queryBuilder = $entityManager->createQueryBuilder();

    $queryBuilder->select('u')
       ->from(MainEntity::class, 'u')
       ->leftJoin('u.third_table', 't')
       ->orderBy('t.score', 'DESC');

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

This works fine (I believe) as I get all the "projects" from main_table + second_table + third_table based on their "project_id".

However the issue is that I cannot find a way to calculate or get each project's "rank" number correctly. I also tried to use a "foreach" and use the "index" as "rank" but that will not work properly because I am using ORMPaginator so each time you click a "page" that "foreach" "index" will reset from 0.

I hope the question is clear enough and gives you a clear understanding of my problem.

Please advise how can I achieve this, and if my whole approach for this is wrong please point it out.

Every advice/hint/solution is highly appreciated.

Mecanik
  • 1,539
  • 1
  • 20
  • 50
  • Basically, you want to do a left join and order by a field in the join table, is that correct? What is missing in the return value of findAllProjects() that you would like to obtain? – Sergio Rinaudo Oct 23 '19 at 15:09
  • @SergioRinaudo Simply ordering by a filed will not get me what I need/want, please look at this: https://stackoverflow.com/questions/3333665/rank-function-in-mysql this is what I basically want. I can order them by "score" which is fine, but I want to get their "rank", 1,2,3,4 based on their score... does that make sense ? – Mecanik Oct 23 '19 at 16:31
  • Actually, you don't want "their rank", but their position in the ordered list. – Ermenegildo Oct 24 '19 at 06:22
  • It would be the same thing, so yes. – Mecanik Oct 24 '19 at 07:00
  • Have you tried add the [OrderBy Annotation](https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/annotations-reference.html#annref_orderby) on the property you wish to order by, e.g. "score" ? (I mean, I see complicated attempts here, but you've also tagged Doctrine, so going for simplicity here -> next up [Criteria](https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/working-with-associations.html#filtering-collections) to filter a Collection of `MainEntity`) – rkeet Oct 28 '19 at 22:01
  • Also, using the OrmPaginator, you should be able to get the position in array (current page, item 4 is the 5th item in array, so "key + 1") and the page with the page limit. So simply in foreach with `$key`: `$paginator->getPage() * $paginator->getLimit() + ((int) $key + 1)` (function names might be slightly off) – rkeet Oct 28 '19 at 22:06
  • @rkeet I thought that solution too (calculate the item using paginator limit and current page), and I think it is still the best/easiest solution, but I didn't look for correct function names of `OrmPaginator`. It would be nice to post your solution, it could be the right one :) – Ermenegildo Oct 29 '19 at 07:53

1 Answers1

1

First off, pay really attention on the column types. All your columns are created by doctrine as varchar(255), which isn't the ideal if those are number on which you'd want to make an order by. This is a snippet of the output of vendor/bin/doctrine-module orm:schema-tool:create --dump-sql:

CREATE TABLE main_entity (
  id VARCHAR(255) NOT NULL,
  PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;

So, the first thing you must do is adding column types to your columns:

/**
 * @ORM\Id
 * @ORM\Column(name="id", type="integer")
 * @ORM\GeneratedValue
 */
protected $id;

I used this data:

third_table                      main_entity
+----+------------+-------+      +----+
| id | project_id | score |      | id |
+----+------------+-------+      +----+
| 1  | 1          | 8     |      | 1  |
| 2  | 2          | 10    |      | 2  |
| 3  | 3          | 6     |      | 3  |
| 4  | 4          | 200   |      | 4  |
+----+------------+-------+      +----+

Concerning the rank, which is substantially the row number, the SQL query is:

SELECT 
   ROW_NUMBER() OVER (ORDER BY t.score DESC) AS r_rank,
   t.score AS score 
FROM main_entity m 
LEFT JOIN third_table t ON m.id = t.project_id 
ORDER BY t.score DESC
-- LIMIT 2 OFFSET 2 -- Limits for pagination
without limits           with limits
+--------+-------+       +--------+-------+
| r_rank | score |       | r_rank | score |
+--------+-------+       +--------+-------+
| 1      | 200   |       | 3      | 8     |
| 2      | 10    |       | 4      | 6     |
| 3      | 8     |       +--------+-------+
| 4      | 6     |
+--------+-------+

Unluckily, ROW_NUMBER is not implemented for SQL queries. If you try this:

$queryBuilder->select(['ROW_NUMBER() OVER (ORDER BY t.score DESC)','t.score'])
        ->from(MainEntity::class, 'u')
        ->leftJoin('u.third_table', 't')
        ->orderBy('t.score', 'DESC');
$queryBuilder->getQuery()->getResult();

You'll get the following error: [Syntax Error] line 0, col 7: Error: Expected known function, got 'ROW_NUMBER' An alternative could be:

$sql = 'SELECT 
            ROW_NUMBER() OVER (ORDER BY t.score DESC) AS r_rank,
            t.score AS score 
        FROM main_entity m 
        LEFT JOIN third_table t ON m.id = t.project_id 
        ORDER BY t.score DESC';
$results = $this->entityManager->getConnection()->executeQuery($sql);

I see that there is something similar in the paginator walkers (?!), but they are not supported for all databases, as you can see in the walker itself:

public function walkSelectStatement(SelectStatement $AST)
{
    if ($this->platformSupportsRowNumber()) {
        return $this->walkSelectStatementWithRowNumber($AST);
    }
    return $this->walkSelectStatementWithoutRowNumber($AST);
}

private function platformSupportsRowNumber()
{
    return $this->platform instanceof PostgreSqlPlatform
        || $this->platform instanceof SQLServerPlatform
        || $this->platform instanceof OraclePlatform
        || $this->platform instanceof SQLAnywherePlatform
        || $this->platform instanceof DB2Platform
        || (method_exists($this->platform, 'supportsRowNumberFunction')
            && $this->platform->supportsRowNumberFunction());
}

As last chance, you could 'add' indexes by yourself, even with the paginator.

If you know the page size and the current page, you can calculate the rank:

$pageSize = 2;
$currentPage = 1;
$queryBuilder = $this->entityManager->createQueryBuilder();
$queryBuilder->select('t.score') // Here I use only the t.score, but you could put the whole class
        ->from(MainEntity::class, 'u')
        ->leftJoin('u.third_table', 't')
        ->orderBy('t.score', 'DESC');
$queryBuilder->setMaxResults($pageSize);
$queryBuilder->setFirstResult(($currentPage - 1) * $pageSize);

$paginator = new \Doctrine\ORM\Tools\Pagination\Paginator($queryBuilder->getQuery());
$adapter = new \DoctrineORMModule\Paginator\Adapter\DoctrinePaginator($paginator);

$results = [];
$currentItem = 1 + ($currentPage - 1) * $pageSize;
foreach($adapter->getPaginator()->getIterator()->getArrayCopy() as $result){
    $results[] = [
        'rank' => $currentItem++,
        'item' => $result
    ];
}
var_dump($results);

Result:

$pageSize = 2;                                        $pageSize = 2;
$currentPage = 1;                                     $currentPage = 2;

Array                                                 Array
(                                                     (
    [0] => Array                                          [0] => Array
        (                                                     (
            [rank] => 1                                           [rank] => 3
            [item] => Array                                       [item] => Array
                (                                                     (
                    [score] => 200                                        [score] => 8
                )                                                     )

        )                                                     )

    [1] => Array                                          [1] => Array
        (                                                     (
            [rank] => 2                                           [rank] => 4
            [item] => Array                                       [item] => Array
                (                                                     (
                    [score] => 10                                         [score] => 6
                )                                                     )

        )                                                     )

)                                                     )
Ermenegildo
  • 1,286
  • 1
  • 12
  • 19