-
Notifications
You must be signed in to change notification settings - Fork 2
Added native query adapter #167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Pagerfanta package. | ||
* | ||
* (c) Pablo Díez <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Pagerfanta\Adapter; | ||
|
||
use Doctrine\ORM\NativeQuery; | ||
use Doctrine\ORM\Query\ResultSetMapping; | ||
use Pagerfanta\Exception\InvalidArgumentException; | ||
|
||
/** | ||
* @author Maxime Veber <[email protected]> | ||
*/ | ||
class DoctrineORMNativeQueryAdapter implements AdapterInterface | ||
{ | ||
/** | ||
* @var NativeQuery | ||
*/ | ||
private $nativeQuery; | ||
|
||
/** | ||
* @var callable | ||
*/ | ||
private $countQueryBuilderModifier; | ||
|
||
/** | ||
* @var callable | ||
*/ | ||
private $sliceQueryBuilderModifier; | ||
|
||
/** | ||
* @param NativeQuery $query A DBAL query builder. | ||
* @param callable $countQueryBuilderModifier A callable to modify the query to count. | ||
* @param callable $sliceQueryBuilderModifier A callable to modify the query to slice it (the default should do the job most part of the time). | ||
* @throws InvalidArgumentException | ||
*/ | ||
public function __construct(NativeQuery $query, $countQueryBuilderModifier = null, $sliceQueryBuilderModifier = null) | ||
{ | ||
if (strpos(strtolower($query->getSQL()), 'select') !== 0) { | ||
throw new InvalidArgumentException('Only SELECT queries can be paginated.'); | ||
} | ||
|
||
if ($countQueryBuilderModifier !== null && !is_callable($countQueryBuilderModifier)) { | ||
throw new InvalidArgumentException('The count query builder modifier must be a callable.'); | ||
} | ||
if ($sliceQueryBuilderModifier !== null && !is_callable($sliceQueryBuilderModifier)) { | ||
throw new InvalidArgumentException('The slice query builder modifier must be a callable.'); | ||
} | ||
|
||
$this->nativeQuery = $query; | ||
$this->countQueryBuilderModifier = $countQueryBuilderModifier; | ||
$this->sliceQueryBuilderModifier = $sliceQueryBuilderModifier; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function getNbResults() | ||
{ | ||
$query = $this->cloneQuery(); | ||
$this->countQueryBuilder($query); | ||
|
||
if ($this->countQueryBuilderModifier !== null) { | ||
call_user_func($this->countQueryBuilderModifier, $query); | ||
} | ||
|
||
$res = $query->getScalarResult(); | ||
|
||
return $res[0]['res']; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function getSlice($offset, $length) | ||
{ | ||
$query = $this->cloneQuery(); | ||
|
||
if ($this->sliceQueryBuilderModifier !== null) { | ||
call_user_func($this->sliceQueryBuilderModifier, $query, $offset, $length); | ||
} else { | ||
$this->sliceQueryBuilder($query, $offset, $length); | ||
} | ||
|
||
return $query->getResult(); | ||
} | ||
|
||
private function cloneQuery() | ||
{ | ||
$query = clone $this->nativeQuery; | ||
$query->setParameters($this->nativeQuery->getParameters()); | ||
|
||
return $query; | ||
} | ||
|
||
private function countQueryBuilder(NativeQuery $query) | ||
{ | ||
$sql = explode(' FROM ', $query->getSql()); | ||
$sql[0] = 'SELECT COUNT(*) AS res'; | ||
$sql = implode(' FROM ', $sql); | ||
|
||
$rsm = new ResultSetMapping(); | ||
$rsm->addScalarResult('res', 'res'); | ||
|
||
$query->setResultSetMapping($rsm); | ||
$query->setSQL($sql); | ||
} | ||
|
||
private function sliceQueryBuilder(NativeQuery $query, $offset, $length) | ||
{ | ||
$sql = $query->getSql(); | ||
$sql .= ' LIMIT ' . $length . ' OFFSET '. $offset; | ||
$query->setSQL($sql); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This does not work. the syntax to limit a SQL result set is different in approximately all platforms, and I think your syntax here does not even work in any of them There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Nek-, could you elaborate on how you'd address the fact that different platforms have different limit syntax? |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
<?php | ||
|
||
namespace Pagerfanta\Tests\Adapter; | ||
|
||
use Doctrine\ORM\NativeQuery; | ||
use Doctrine\ORM\Query\ResultSetMappingBuilder; | ||
use Doctrine\ORM\Tools\SchemaTool; | ||
use Pagerfanta\Adapter\DoctrineORMNativeQueryAdapter; | ||
use Pagerfanta\Tests\Adapter\DoctrineORM\DoctrineORMTestCase; | ||
use Pagerfanta\Tests\Adapter\DoctrineORM\Group; | ||
use Pagerfanta\Tests\Adapter\DoctrineORM\Person; | ||
use Pagerfanta\Tests\Adapter\DoctrineORM\User; | ||
|
||
class DoctrineORMNativeQueryAdapterTest extends DoctrineORMTestCase | ||
{ | ||
private $user1; | ||
private $user2; | ||
|
||
public function setUp() | ||
{ | ||
parent::setUp(); | ||
|
||
$schemaTool = new SchemaTool($this->entityManager); | ||
$schemaTool->createSchema(array( | ||
$this->entityManager->getClassMetadata('Pagerfanta\Tests\Adapter\DoctrineORM\User'), | ||
$this->entityManager->getClassMetadata('Pagerfanta\Tests\Adapter\DoctrineORM\Group'), | ||
$this->entityManager->getClassMetadata('Pagerfanta\Tests\Adapter\DoctrineORM\Person'), | ||
)); | ||
|
||
$this->user1 = $user = new User(); | ||
$this->user2 = $user2 = new User(); | ||
$group1 = new Group(); | ||
$group2 = new Group(); | ||
$group3 = new Group(); | ||
$user->groups[] = $group1; | ||
$user->groups[] = $group2; | ||
$user->groups[] = $group3; | ||
$user2->groups[] = $group1; | ||
$author1 = new Person(); | ||
$author1->name = 'Foo'; | ||
$author1->biography = 'Baz bar'; | ||
$author2 = new Person(); | ||
$author2->name = 'Bar'; | ||
$author2->biography = 'Bar baz'; | ||
|
||
$this->entityManager->persist($user); | ||
$this->entityManager->persist($user2); | ||
$this->entityManager->persist($group1); | ||
$this->entityManager->persist($group2); | ||
$this->entityManager->persist($group3); | ||
$this->entityManager->persist($author1); | ||
$this->entityManager->persist($author2); | ||
$this->entityManager->flush(); | ||
} | ||
|
||
public function testAdapterCount() | ||
{ | ||
$sql = "SELECT * FROM User u"; | ||
|
||
$rsm = new ResultSetMappingBuilder($this->entityManager); | ||
$rsm->addRootEntityFromClassMetadata('Pagerfanta\Tests\Adapter\DoctrineORM\User', 'u'); | ||
|
||
$nq = $this->entityManager->createNativeQuery($sql, $rsm); | ||
|
||
$adapter = new DoctrineORMNativeQueryAdapter($nq); | ||
$this->assertEquals(2, $adapter->getNbResults()); | ||
} | ||
|
||
public function testAdapterCountFetchJoin() | ||
{ | ||
$count = function (NativeQuery $query) { | ||
$query->setSQL('SELECT COUNT(*) AS res FROM User u'); | ||
}; | ||
|
||
$rsm = new ResultSetMappingBuilder($this->entityManager); | ||
$rsm->addRootEntityFromClassMetadata('Pagerfanta\Tests\Adapter\DoctrineORM\User', 'u'); | ||
$rsm->addJoinedEntityFromClassMetadata('Pagerfanta\Tests\Adapter\DoctrineORM\Group', 'g', 'u', 'groups', array('id' => 'user_id')); | ||
$sql = "SELECT u.id AS id, g.id AS user_id" . | ||
" FROM User u INNER JOIN user_group ug ON u.id = ug.user_id LEFT JOIN groups g ON g.id = ug.group_id" | ||
; | ||
|
||
$nq = $this->entityManager->createNativeQuery($sql, $rsm); | ||
$adapter = new DoctrineORMNativeQueryAdapter($nq, $count); | ||
$this->assertEquals(2, $adapter->getNbResults()); | ||
} | ||
|
||
public function testGetSlice() | ||
{ | ||
$sql = "SELECT * FROM User u"; | ||
|
||
$rsm = new ResultSetMappingBuilder($this->entityManager); | ||
$rsm->addRootEntityFromClassMetadata('Pagerfanta\Tests\Adapter\DoctrineORM\User', 'u'); | ||
|
||
$nq = $this->entityManager->createNativeQuery($sql, $rsm); | ||
|
||
$adapter = new DoctrineORMNativeQueryAdapter($nq); | ||
$this->assertEquals(1, count( $adapter->getSlice(0, 1)) ); | ||
$this->assertEquals(2, count( $adapter->getSlice(0, 10)) ); | ||
$this->assertEquals(1, count( $adapter->getSlice(1, 1)) ); | ||
} | ||
|
||
public function testGetSliceFetchJoin() | ||
{ | ||
$rsm = new ResultSetMappingBuilder($this->entityManager); | ||
$rsm->addRootEntityFromClassMetadata('Pagerfanta\Tests\Adapter\DoctrineORM\User', 'u'); | ||
$rsm->addJoinedEntityFromClassMetadata('Pagerfanta\Tests\Adapter\DoctrineORM\Group', 'g', 'u', 'groups', array('id' => 'user_id')); | ||
$sql = "SELECT u.id AS id, g.id AS user_id" . | ||
" FROM User u INNER JOIN user_group ug ON u.id = ug.user_id LEFT JOIN groups g ON g.id = ug.group_id" | ||
; | ||
|
||
$nq = $this->entityManager->createNativeQuery($sql, $rsm); | ||
$adapter = new DoctrineORMNativeQueryAdapter($nq); | ||
$this->assertEquals(1, count( $adapter->getSlice(0, 1)) ); | ||
$this->assertEquals(2, count( $adapter->getSlice(0, 10)) ); | ||
$this->assertEquals(1, count( $adapter->getSlice(1, 1)) ); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will fail when you have a sub-query in the SELECT part.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will fail in a lot of case actually. And the implementation forces to apply this even when providing a custom modifier, meaning there is no way to get rid of this broken implementation.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At first the method
countQueryBuilder
was originally called on theelse
statement of thisif
https://github.com/whiteoctober/Pagerfanta/pull/167/files#diff-47292a5a239d0185dbe662f2f1b0ed81R70 .But then I wrote the test and I noticed that having the result set mapping that corresponds to the count is more than useful. That's why I changed it to make it like that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Nek-, you've explained why you'd like the
countQueryBuilder
method to always be called (even if a modifier is provided), so thank you for that, but I'm not sure you've addressed @stof's concerns that the implentnation of that method will break in a lot of cases.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The best answer I can give here is the detection of too complex queries and throw an exception for these cases. Otherwise good thing to do is to build an SQL analyzer.. :')