Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

[DDC-1398] Extra-lazy get for indexed associations #706

Merged
merged 7 commits into from

4 participants

@sandermarechal

If an association is EXTRA_LAZY and has an indexBy, then
you can call get() without loading the entire collection.

@sandermarechal sandermarechal [DDC-1398] Extra-lazy get for indexed associations
If an association is EXTRA_LAZY and has an indexBy, then
you can call get() without loading the entire collection.
523697d
@beberlei
Owner

<3

lib/Doctrine/ORM/Persisters/ManyToManyPersister.php
@@ -38,6 +38,24 @@ class ManyToManyPersister extends AbstractCollectionPersister
*
* @override
*/
+ public function get(PersistentCollection $coll, $index)
+ {
+ $mapping = $coll->getMapping();
+ $uow = $this->em->getUnitOfWork();
+ $persister = $uow->getEntityPersister($mapping['targetEntity']);
+
+ if (!isset($mapping['indexBy'])) {
+ throw new \BadMethodCallException("Selecting a collection by index is only supported on indexed collections.");
+ }
+
+ return current($persister->load(array($mapping['indexBy'] => $index), null, null, array(), 0, 1));
@stof
stof added a note

you need to handle the case of non-existent keys properly: the Collection interface wants to get null in such case, not false (which is what current returns when the array is empty)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/Doctrine/ORM/PersistentCollection.php
@@ -517,6 +517,14 @@ public function indexOf($element)
*/
public function get($key)
{
+ if ( ! $this->initialized
+ && $this->association['fetch'] === Mapping\ClassMetadataInfo::FETCH_EXTRA_LAZY
+ && isset($this->association['indexBy'])
+ ) {
+ $persister = $this->em->getUnitOfWork()->getCollectionPersister($this->association);
@Ocramius Owner

I think you can get rid of $persister too here. Otherwise the PR looks clean!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@stof

you could also implement containsKey this way, checking if an element is found

...rine/Tests/ORM/Functional/ExtraLazyCollectionTest.php
@@ -512,6 +517,38 @@ public function testSliceOnDirtyCollection()
$this->assertEquals($qc + 1, $this->getCurrentQueryCount());
}
+ /**
+ * @group DDC-1398
+ */
+ public function testGetIndexByOneToMany()
+ {
+ $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId);
+ /* @var $user CmsUser */
+
+ $queryCount = $this->getCurrentQueryCount();
+
+ $user->articles->get($this->articleId);
@beberlei Owner

can you test for the actual data being returned as well? in the second test as well

@stof
stof added a note

and please also add a test for non-existent keys

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
sandermarechal added some commits
@sandermarechal sandermarechal Return NULL for non-existent keys
The load() function already returns just one entity or NULL, so
the current() is not needed and the result can be returned directly.
3555007
@sandermarechal sandermarechal Test actual data returned by get() 647c5e2
@sandermarechal sandermarechal Get rid of variable 53c9ffd
@sandermarechal

I think I fixed all the things you pointed out. I'm looking at implementing containsKey as well.

@sandermarechal

I am not sure what the best way is to handle containsKey. I see three possible options:

  1. Simply call get from containsKey and check for NULL. This is simple to implement but it will load the actuall entity (which may have FETCH_EAGER associations, causing more data to be hydrated, etcetera).

  2. Query the database directly. This is what I have been working on so far. It works, but looks ugly. There is significant code overlap between containsKey and count and I don't see a non-ugly way to reduce it, except for option 3.

  3. Extend the count API to accept additional conditions and/or Criteria. In this case containsKey could simply use count with an extra condition and check if the result >= 0. Downside is that it is more work and that it is an addition to the API. On the upside, this sounds like a nice feature to have. People would be able to do things like this on a collection, and it would work properly on extra lazy collections as well:

    $numUsers = $group->users->count(array('active' => true));
    

So, what do you suggest?

@beberlei
Owner

@sandermarechal Lets keep containsKey for another time, we have something very useful here already. One thing I would like to see is the Persister Collections to use find() instead of findBy() to use the identity map, if the index-by is the identifier field. Otherwise its already awesome

@sandermarechal

@beberlei: I'm sorry, I don't quite understand what you mean. I'm not using find() in this PR but I am using load() directly.

Do you mean fixing the TODO at BasicEntityPersister->load() on line 742?

@beberlei
Owner

@sandermarechal yes, please use the EntityManager#find instead if index-by == identifier field. This will greatly enhance performance in some use-cases.

@stof

@sandermarechal regarding your propsal above, I don't think count() should be changed. It is the implementation of Countable

@sandermarechal

@beberlei Done

@stof Good point. I didn't think about Countable. Anyway, I will make a separate PR for containsKey so this can be merged.

@beberlei
Owner

@sandermarechal i am afraid you need to add more tests now ;) the article stuff uses articleIds, so its never triggering the collection persister get calls anymore

@beberlei beberlei merged commit eaf8fd3 into doctrine:master

1 check failed

Details default The Travis CI build failed
@sandermarechal sandermarechal deleted the sandermarechal:extra-lazy-get branch
@sandermarechal sandermarechal referenced this pull request
Merged

Fix extra lazy get #710

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jun 20, 2013
  1. @sandermarechal

    [DDC-1398] Extra-lazy get for indexed associations

    sandermarechal authored
    If an association is EXTRA_LAZY and has an indexBy, then
    you can call get() without loading the entire collection.
  2. @sandermarechal

    Return NULL for non-existent keys

    sandermarechal authored
    The load() function already returns just one entity or NULL, so
    the current() is not needed and the result can be returned directly.
  3. @sandermarechal
  4. @sandermarechal

    Get rid of variable

    sandermarechal authored
  5. @sandermarechal
  6. @sandermarechal
  7. @sandermarechal
This page is out of date. Refresh to see the latest.
View
11 lib/Doctrine/ORM/PersistentCollection.php
@@ -517,6 +517,17 @@ public function indexOf($element)
*/
public function get($key)
{
+ if ( ! $this->initialized
+ && $this->association['fetch'] === Mapping\ClassMetadataInfo::FETCH_EXTRA_LAZY
+ && isset($this->association['indexBy'])
+ ) {
+ if (!$this->typeClass->isIdentifierComposite && $this->typeClass->isIdentifier($this->association['indexBy'])) {
+ return $this->em->find($this->typeClass->name, $key);
+ }
+
+ return $this->em->getUnitOfWork()->getCollectionPersister($this->association)->get($this, $key);
+ }
+
$this->initialize();
return $this->coll->get($key);
View
18 lib/Doctrine/ORM/Persisters/ManyToManyPersister.php
@@ -38,6 +38,24 @@ class ManyToManyPersister extends AbstractCollectionPersister
*
* @override
*/
+ public function get(PersistentCollection $coll, $index)
+ {
+ $mapping = $coll->getMapping();
+ $uow = $this->em->getUnitOfWork();
+ $persister = $uow->getEntityPersister($mapping['targetEntity']);
+
+ if (!isset($mapping['indexBy'])) {
+ throw new \BadMethodCallException("Selecting a collection by index is only supported on indexed collections.");
+ }
+
+ return $persister->load(array($mapping['indexBy'] => $index), null, null, array(), 0, 1);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @override
+ */
protected function getDeleteRowSQL(PersistentCollection $coll)
{
$columns = array();
View
18 lib/Doctrine/ORM/Persisters/OneToManyPersister.php
@@ -33,6 +33,24 @@
class OneToManyPersister extends AbstractCollectionPersister
{
/**
+ * {@inheritdoc}
+ *
+ * @override
+ */
+ public function get(PersistentCollection $coll, $index)
+ {
+ $mapping = $coll->getMapping();
+ $uow = $this->em->getUnitOfWork();
+ $persister = $uow->getEntityPersister($mapping['targetEntity']);
+
+ if (!isset($mapping['indexBy'])) {
+ throw new \BadMethodCallException("Selecting a collection by index is only supported on indexed collections.");
+ }
+
+ return $persister->load(array($mapping['indexBy'] => $index), null, null, array(), 0, 1);
+ }
+
+ /**
* Generates the SQL UPDATE that updates a particular row's foreign
* key to null.
*
View
104 tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php
@@ -17,6 +17,10 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase
private $groupId;
private $articleId;
+ private $groupname;
+ private $topic;
+ private $phonenumber;
+
public function setUp()
{
$this->useModelSet('cms');
@@ -24,7 +28,11 @@ public function setUp()
$class = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsUser');
$class->associationMappings['groups']['fetch'] = ClassMetadataInfo::FETCH_EXTRA_LAZY;
+ $class->associationMappings['groups']['indexBy'] = 'name';
$class->associationMappings['articles']['fetch'] = ClassMetadataInfo::FETCH_EXTRA_LAZY;
+ $class->associationMappings['articles']['indexBy'] = 'topic';
+ $class->associationMappings['phonenumbers']['fetch'] = ClassMetadataInfo::FETCH_EXTRA_LAZY;
+ $class->associationMappings['phonenumbers']['indexBy'] = 'phonenumber';
$class = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsGroup');
$class->associationMappings['users']['fetch'] = ClassMetadataInfo::FETCH_EXTRA_LAZY;
@@ -39,6 +47,11 @@ public function tearDown()
$class = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsUser');
$class->associationMappings['groups']['fetch'] = ClassMetadataInfo::FETCH_LAZY;
$class->associationMappings['articles']['fetch'] = ClassMetadataInfo::FETCH_LAZY;
+ $class->associationMappings['phonenumbers']['fetch'] = ClassMetadataInfo::FETCH_LAZY;
+
+ unset($class->associationMappings['groups']['indexBy']);
+ unset($class->associationMappings['articles']['indexBy']);
+ unset($class->associationMappings['phonenumbers']['indexBy']);
$class = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsGroup');
$class->associationMappings['users']['fetch'] = ClassMetadataInfo::FETCH_LAZY;
@@ -174,8 +187,8 @@ public function testSliceInitializedCollection()
$this->assertEquals($queryCount + 1, $this->getCurrentQueryCount());
$this->assertEquals(2, count($someGroups));
- $this->assertTrue($user->groups->contains($someGroups[0]));
- $this->assertTrue($user->groups->contains($someGroups[1]));
+ $this->assertTrue($user->groups->contains(array_shift($someGroups)));
+ $this->assertTrue($user->groups->contains(array_shift($someGroups)));
}
/**
@@ -512,6 +525,72 @@ public function testSliceOnDirtyCollection()
$this->assertEquals($qc + 1, $this->getCurrentQueryCount());
}
+ /**
+ * @group DDC-1398
+ */
+ public function testGetIndexByIdentifier()
+ {
+ $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId);
+ /* @var $user CmsUser */
+
+ $queryCount = $this->getCurrentQueryCount();
+
+ $phonenumber = $user->phonenumbers->get($this->phonenumber);
+
+ $this->assertFalse($user->phonenumbers->isInitialized());
+ $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount());
+ $this->assertSame($phonenumber, $this->_em->find('Doctrine\Tests\Models\CMS\CmsPhonenumber', $this->phonenumber));
+
+ $article = $user->phonenumbers->get($this->phonenumber);
+ $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount(), "Getting the same entity should not cause an extra query to be executed");
+ }
+
+ /**
+ * @group DDC-1398
+ */
+ public function testGetIndexByOneToMany()
+ {
+ $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId);
+ /* @var $user CmsUser */
+
+ $queryCount = $this->getCurrentQueryCount();
+
+ $article = $user->articles->get($this->topic);
+
+ $this->assertFalse($user->articles->isInitialized());
+ $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount());
+ $this->assertSame($article, $this->_em->find('Doctrine\Tests\Models\CMS\CmsArticle', $this->articleId));
+ }
+
+ /**
+ * @group DDC-1398
+ */
+ public function testGetIndexByManyToMany()
+ {
+ $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId);
+ /* @var $user CmsUser */
+
+ $queryCount = $this->getCurrentQueryCount();
+
+ $group = $user->groups->get($this->groupname);
+
+ $this->assertFalse($user->groups->isInitialized());
+ $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount());
+ $this->assertSame($group, $this->_em->find('Doctrine\Tests\Models\CMS\CmsGroup', $this->groupId));
+ }
+
+ /**
+ * @group DDC-1398
+ */
+ public function testGetNonExistentIndexBy()
+ {
+ $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId);
+ /* @var $user CmsUser */
+
+ $this->assertNull($user->articles->get(-1));
+ $this->assertNull($user->groups->get(-1));
+ }
+
private function loadFixture()
{
$user1 = new \Doctrine\Tests\Models\CMS\CmsUser();
@@ -561,23 +640,36 @@ private function loadFixture()
$this->_em->persist($group3);
$article1 = new \Doctrine\Tests\Models\CMS\CmsArticle();
- $article1->topic = "Test";
- $article1->text = "Test";
+ $article1->topic = "Test1";
+ $article1->text = "Test1";
$article1->setAuthor($user1);
$article2 = new \Doctrine\Tests\Models\CMS\CmsArticle();
- $article2->topic = "Test";
- $article2->text = "Test";
+ $article2->topic = "Test2";
+ $article2->text = "Test2";
$article2->setAuthor($user1);
$this->_em->persist($article1);
$this->_em->persist($article2);
+ $phonenumber1 = new \Doctrine\Tests\Models\CMS\CmsPhonenumber();
+ $phonenumber1->phonenumber = '12345';
+
+ $phonenumber2 = new \Doctrine\Tests\Models\CMS\CmsPhonenumber();
+ $phonenumber2->phonenumber = '67890';
+
+ $this->_em->persist($phonenumber1);
+ $this->_em->persist($phonenumber2);
+
$this->_em->flush();
$this->_em->clear();
$this->articleId = $article1->id;
$this->userId = $user1->getId();
$this->groupId = $group1->id;
+
+ $this->groupname = $group1->name;
+ $this->topic = $article1->topic;
+ $this->phonenumber = $phonenumber1->phonenumber;
}
}
Something went wrong with that request. Please try again.