Skip to content
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

Second level cache #808

Merged
merged 9 commits into from
Dec 16, 2013
Merged

Second level cache #808

merged 9 commits into from
Dec 16, 2013

Conversation

FabioBatSilva
Copy link
Member

Hi guys,

I believe, We are coming to an end.. ;)
#580, rebased and reopened to make easier the final review.

Cache strategies

* READ_ONLY (DEFAULT)   : ReadOnly cache can do reads, inserts and deletes, cannot perform updates or employ any locks.
* NONSTRICT_READ_WRITE  : Nonstrict Read Write Cache doesn’t employ any locks but can do reads, inserts , updates and deletes.
* READ_WRITE            : Read Write cache employs locks the entity before update/delete.

classes / interfaces

  • Region :
    Defines a contract for accessing a entity/collection data cache. (Doesn’t employ any locks)
  • ConcurrentRegion :
    Defines contract for concurrently managed data region. (Locks the data before update/delete.)
  • TimestampRegion :
    Holds timestamps of the most recent entities/tables updates. These timestamps are used as part of the cache for persister queries (findAll, findBy, ...).
  • CacheKey / EntityCacheKey / CollectionCacheKey / QueryCacheKey / TimestampCacheKey:
    Defines entity / collection key to be stored in the cache region.
  • EntityHydrator / CollectionHidrator
    Build cache entries and rebuild entities/colection from cache
  • CacheFactory
    Factory from second level cache components

Collection Caching

The most common use case is to cache entities. But we can also cache relationships.
A “collection cache” caches the primary keys of entities that are members of a collection (OneToMany/ManyToMany).
and each element will be cached into its region.

Only identifiers will be cached for collection. When a collection is read from the second level cache it will create proxies based on the cached identifiers, if the application needs to access an element, Doctrine will go to the cache to load the element data.

Query Cache

The query cache does not cache the state of the actual entities in the result set;
it caches only identifier values for an individual query.
So the query cache should always be used in conjunction with the second-level cache.

Persister Query (findAll, findBy, ...).

As well as Query Cache all persister queries store only identifier values for an individual query.
All persister use a single timestamps cache region keeps track of the last update for each persister,
When a query is loaded from cache, the timestamp region is checked for the last update for that persister.
Using the last update timestamps as part of the query key invalidate the cache key when an update occurs.

OPERATIONS

INSERT :

*************************************************************************************************
                    | READ-ONLY             | NONSTRICT-READ-WRITE      | READ-WRITE            |
-------------------------------------------------------------------------------------------------
pre-insert          |                       |                           |                       |
-------------------------------------------------------------------------------------------------
on-insert           |                       |                           |                       |
-------------------------------------------------------------------------------------------------
after-transaction   | add item to the cache | add item to the cache     | add item to the cache |
-------------------------------------------------------------------------------------------------

UPDATE :

*************************************************************************************************
                    | READ-ONLY             | NONSTRICT-READ-WRITE      | READ-WRITE            |
-------------------------------------------------------------------------------------------------
pre-update          |                       |                           | lock item             |
-------------------------------------------------------------------------------------------------
on-update           | throws exception      |                           |                       |
-------------------------------------------------------------------------------------------------
after-transaction   |                       |  update item cache        | remove item cache     |
-------------------------------------------------------------------------------------------------

DELETE :

*************************************************************************************************
                    | READ-ONLY             | NONSTRICT-READ-WRITE      | READ-WRITE            |
-------------------------------------------------------------------------------------------------
pre-remove          |                       |                           |                       |
-------------------------------------------------------------------------------------------------
on-remove           |                       |                           | lock item             |
-------------------------------------------------------------------------------------------------
after-transaction   | remove item cache     |  remove item cache        | remove item cache     |
-------------------------------------------------------------------------------------------------

USAGE :

<?php

/**
 * @Entity
 * @Cache("NONSTRICT_READ_WRITE")
 */
class State
{
    /**
     * @Id
     * @GeneratedValue
     * @Column(type="integer")
     */
    protected $id;
    /**
     * @Column
     */
    protected $name;
    /**
     * @Cache()
     * @ManyToOne(targetEntity="Country")
     * @JoinColumn(name="country_id", referencedColumnName="id")
     */
    protected $country;
    /**
     * @Cache()
     * @OneToMany(targetEntity="City", mappedBy="state")
     */
    protected $cities;
}
<?php

$em->persist(new State($name, $country));
$em->flush();                                // Put into cache

$em->clear();                                // Clear entity manager

$state   = $em->find('Entity\State', 1);     // Retreive item from cache
$country = $state->getCountry();             // Retreive item from cache
$cities  = $state->getCities();              // Load from database and put into cache

$state->setName("New Name");
$em->persist($state);
$em->flush();                                // Update item cache

$em->clear();                                // Clear entity manager

$em->find('Entity\State', 1)->getCities();   // Retreive from cache


$em->getCache()->containsEntity('Entity\State', $state->getId())  // Check if the cache exists
$em->getCache()->evictEntity('Entity\State', $state->getId());    // Remove an entity from cache
$em->getCache()->evictEntityRegion('Entity\State');               // Remove all entities from cache

$em->getCache()->containsCollection('Entity\State', 'cities', $state->getId());   // Check if the cache exists        
$em->getCache()->evictCollection('Entity\State', 'cities', $state->getId());      // Remove an entity collection from cache
$em->getCache()->evictCollectionRegion('Entity\State', 'cities');                 // Remove all collections from cache
<?php

    // Execute database query, store query cache and entity cache into 2lc
    $result = $em->createQuery('SELECT s FROM Entity\State s ORDER BY s.name')
        ->setCacheable(true)
        ->getResult();
<?php

// load from database and store cache query key hashing the query + parameters + last timestamp cache region..
$entities   = $em->getRepository('Entity\Country')->findAll();

// load from cache..
$entities   = $em->getRepository('Entity\Country')->findAll();

// update the timestamp cache region for Country
$em->persist(new Country('zombieland'));
$em->flush();
$em->clear();  

// Reload the query from database.
// The timestamp cache region update invalidates the query key
$entities   = $em->getRepository('Entity\Country')->findAll();

@doctrinebot
Copy link

Hello,

thank you for creating this pull request. I have automatically opened an issue
on our Jira Bug Tracker for you. See the issue link:

http://www.doctrine-project.org/jira/browse/DDC-2716

We use Jira to track the state of pull requests and the versions they got
included in.

@FabioBatSilva
Copy link
Member Author

Performance tests CACHE DISABLED

doctrine/master

phpunit --group performance

.testIssue - 0.014856100082397 seconds
.testSimpleQueryScalarHydrationPerformance10000Rows - 0.25800895690918 seconds
.testSimpleQueryArrayHydrationPerformance10000Rows - 0.32865500450134 seconds
.testMixedQueryFetchJoinArrayHydrationPerformance10000Rows - 0.69533610343933 seconds
.testSimpleQueryPartialObjectHydrationPerformance10000Rows - 0.74423408508301 seconds
.testSimpleQueryFullObjectHydrationPerformance10000Rows - 2.6838490962982 seconds
.testMixedQueryFetchJoinPartialObjectHydrationPerformance2000Rows - 0.36051917076111 seconds
.testMixedQueryFetchJoinFullObjectHydrationPerformance2000Rows - 0.72839307785034 seconds
.99 CompanyContract: 0.017542
99 CompanyContract: 0.014437
. Inserted 10000 objects in 4.7838621139526 seconds
.100 CmsArticle findAll(): 0.015257
100 CmsArticle findAll(): 0.012923
100 CmsArticle find(): 0.126162
100 CmsArticle find(): 0.030804
.100 CmsGroup: 0.006855
100 CmsGroup: 0.006526
.100 CmsUser: 0.017517
100 CmsUser: 0.024142
.testProxyInstantiationPerformance - 0.66102814674377 seconds with Doctrine\Tests\Models\CMS\CmsEmployee
.testProxyInstantiationPerformance - 1.4936001300812 seconds with Doctrine\Tests\Models\CMS\CmsUser
.testProxyForcedInitializationPerformance - 2.1531109809875 seconds with Doctrine\Tests\Models\CMS\CmsEmployee
.testProxyForcedInitializationPerformance - 3.205668926239 seconds with Doctrine\Tests\Models\CMS\CmsUser
. Compute ChangeSet 100 objects in 0.03258490562439 seconds

second-level-cache

phpunit --group performance

.testIssue - 0.012160062789917 seconds
.testSimpleQueryScalarHydrationPerformance10000Rows - 0.24768280982971 seconds
.testSimpleQueryArrayHydrationPerformance10000Rows - 0.33675599098206 seconds
.testMixedQueryFetchJoinArrayHydrationPerformance10000Rows - 0.68599414825439 seconds
.testSimpleQueryPartialObjectHydrationPerformance10000Rows - 0.71943783760071 seconds
.testSimpleQueryFullObjectHydrationPerformance10000Rows - 2.6718890666962 seconds
.testMixedQueryFetchJoinPartialObjectHydrationPerformance2000Rows - 0.34043478965759 seconds
.testMixedQueryFetchJoinFullObjectHydrationPerformance2000Rows - 0.72354388237 seconds
.99 CompanyContract: 0.016345
99 CompanyContract: 0.014366
. Inserted 10000 objects in 4.8366978168488 seconds
.100 CmsArticle findAll(): 0.015051
100 CmsArticle findAll(): 0.012165
100 CmsArticle find(): 0.130350
100 CmsArticle find(): 0.036123
.100 CmsGroup: 0.006865
100 CmsGroup: 0.006594
.100 CmsUser: 0.017938
100 CmsUser: 0.020916
.testProxyInstantiationPerformance - 0.70478510856628 seconds with Doctrine\Tests\Models\CMS\CmsEmployee
.testProxyInstantiationPerformance - 1.4089779853821 seconds with Doctrine\Tests\Models\CMS\CmsUser
.testProxyForcedInitializationPerformance - 2.3513119220734 seconds with Doctrine\Tests\Models\CMS\CmsEmployee
.testProxyForcedInitializationPerformance - 3.2526369094849 seconds with Doctrine\Tests\Models\CMS\CmsUser
. Compute ChangeSet 100 objects in 0.03451681137085 seconds

@FabioBatSilva
Copy link
Member Author

Second level cache performance test using mysql and apc cache

phpunit -c phpunit.mysql.xml tests/Doctrine/Tests/ORM/Performance/SecondLevelCacheTest.php 

.
testFindEntityWithoutCache
[0.064726] persist 500 countries
[0.887743] find 500 countries (10 times)
--------------------------------------------------
.
testFindEntityWithCache
[0.065341] persist 500 countries
[0.151151] find 500 countries (10 times)
--------------------------------------------------
.
testFindAllEntityWithoutCache
[0.008606] persist 50 countries
[0.112103] find 50 countries (100 times)
--------------------------------------------------
.
testFindAllEntityWithCache
[0.006810] persist 50 countries
[0.101012] find 50 countries (100 times)
--------------------------------------------------
.
testFindEntityOneToManyWithoutCache
[0.085609] persist 15 states and 450 cities
[0.061725] find 15 states and 450 cities (50 times)
--------------------------------------------------
.
testFindEntityOneToManyWithCache
[0.091436] persist 15 states and 450 cities
[0.054899] find 15 states and 450 cities (50 times)
--------------------------------------------------
.
testQueryEntityWithoutCache
[0.060757] persist 500 countries
[0.875905] select 500 countries (100 times)
--------------------------------------------------
.
testQueryEntityWithCache
[0.079409] persist 500 countries
[0.781645] select 500 countries (100 times)
--------------------------------------------------

The Second Level Cache
======================

The Second Level Cache is designed to reduces the amount of necessary database access.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo "reduces"

@guilhermeblanco
Copy link
Member

I'd not have an ON/OFF flag for SL cache.
This is very tricky, but I don't see how it can be turned off. I can highlight a very interesting situation:

I'm a developer, I turned off SL Cache and want to use Result Cache. This means that we'll store the list of IDs and also each individual Entity on its own cache entry. How it should behave? Are we using the individual Entity entries or not?
Now, I used Result Cache and cached a query. Now I do a find() on a cached Entity. Should it use the cache, yes or no?
Finally, I update an Entity that is cached by Result Cache. Will it be updated on Entity cache entry?

So, this means to me there's no way to completely disable the SL cache because of consequences it may happen.

@FabioBatSilva
Copy link
Member Author

@guilhermeblanco indeed, this ON/OFF might be confuse..

There are 3 points where we check if the config cache is enabled uow, em, query.
They are small optimization to avoid extra check when the cache is not been used.
if you guys agree i'm OK turning it on..

@stof
Copy link
Member

stof commented Oct 3, 2013

By reading the section on collection caching in the PR description (I haven't looked at the diff at all yet), I'm wondering what happens for the collection cache when an item is removed. Will its id be removed from all collection caches or no ? If no, it will create a proxy to a non-existent object, which will then trigger an exception when being initialized

@FabioBatSilva
Copy link
Member Author

@stof Both sides should be aware of this kind of change.. http://planet.jboss.org/post/collection_caching_in_the_hibernate_second_level_cache

But i think in this case the missing entity cache will trigger another query to load the collection again,
https://github.com/FabioBatSilva/doctrine2/blob/slc/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToManyTest.php#L128.

<xs:complexType name="entity">
<xs:sequence>
<xs:element name="cache" type="orm:cache" minOccurs="0" maxOccurs="unbounded"/>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maxOccurs="1" everywhere! =D

@FabioBatSilva
Copy link
Member Author

@doctrine/team-doctrine2 thoughts ?

@skydiablo
Copy link

unbelivebal! will it available in doctrine 2.5?

@FabioBatSilva
Copy link
Member Author

@skydiablo That is the goal..

@skydiablo
Copy link

is there a realese date for doctrine 2.5?

@@ -742,11 +884,10 @@ public function iterate($parameters = null, $hydrationMode = null)
$this->setParameters($parameters);
}

$rsm = $this->_resultSetMapping ?: $this->getResultSetMapping();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this will always somehow be extended and manipulating _resultSetMapping, I wouldn't make this ternary anywhere. Seens in 3 places on this file, and should purely refer to public method.

), $arguments);
}

public function testInplementsCacheFactory()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inplements -> Implements

@pborreli
Copy link
Contributor

done a little review, sorry for the spam

@FabioBatSilva
Copy link
Member Author

@pborreli, Thanks for reviewing

guilhermeblanco added a commit that referenced this pull request Dec 16, 2013
@guilhermeblanco guilhermeblanco merged commit b081e56 into doctrine:master Dec 16, 2013
@CoolLamer
Copy link

Hi I found problem:
when I update cached entitiy, entity lost OneToOne relationship
Example:

$driver = $em->find('CachedDoctrine\Entities\Driver', 1);  //Get cached entity
dump($driver->getUserProfile()); //Return User
$driver->setName('Franta');
$em->flush();
$em->clear();
$driver = $em->find('CachedDoctrine\Entities\Driver', 1);
dump($driver->getUserProfile() ); //Return null

Sample project
https://github.com/CoolLamer/CachedDoctrine

@Ocramius
Copy link
Member

@CoolLamer please open a separate issue for that on http://www.doctrine-project.org/jira/browse/DDC and provide a test case there (we can't check out an entire project for that - we have a test suite to verify this kind of behavior ;) )

fabpot added a commit to symfony/symfony that referenced this pull request Dec 28, 2013
…re not an EM's child (FabioBatSilva)

This PR was merged into the 2.5-dev branch.

Discussion
----------

[Bridge] [DoctrineExtension] Allow cache drivers that are not an EM's child

| Q             | A
| ------------- | ---
| Bug fix?      | [no]
| New feature?  | [yes]
| BC breaks?    | [no]
| Deprecations? | [no]
| Tests pass?   | [yes|]
| Fixed tickets | []
| License       | MIT
| Doc PR        | [doctrine/orm#808, doctrine/DoctrineBundle#224]

Commits
-------

7528e4c Allow cache drivers that are not an EM's child
fabpot added a commit to symfony/doctrine-bridge that referenced this pull request Dec 28, 2013
…re not an EM's child (FabioBatSilva)

This PR was merged into the 2.5-dev branch.

Discussion
----------

[Bridge] [DoctrineExtension] Allow cache drivers that are not an EM's child

| Q             | A
| ------------- | ---
| Bug fix?      | [no]
| New feature?  | [yes]
| BC breaks?    | [no]
| Deprecations? | [no]
| Tests pass?   | [yes|]
| Fixed tickets | []
| License       | MIT
| Doc PR        | [doctrine/orm#808, doctrine/DoctrineBundle#224]

Commits
-------

7528e4c Allow cache drivers that are not an EM's child
@vvh-empora
Copy link
Contributor

i have read, Doctrine 2.5BETA is released. but no words about this great caching feature ?

@deeky666
Copy link
Member

@vvh-empora This refers to DBAL, not to ORM. ORM is currently not yet in BETA stage. So expect news for this feature as soon as ORM goes BETA :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.