AbstractProtected ReadonlycompanionProtected ReadonlydatabaseProtected ReadonlyentityProtected ReadonlyentityProtected ReadonlyentityProtected ReadonlymetricsProtected ReadonlymutationProtected ReadonlymutationProtected ReadonlyprivacyProtected ReadonlyqueryProtected ReadonlyviewerProtectedexecuteProtectedexecuteProtectedexecuteProtectedvalidate
Base class for entity mutators. Mutators are builder-like class instances that are responsible for creating, updating, and deleting entities, and for calling out to the loader at appropriate times to invalidate the cache(s). The loader is responsible for deciding which cache entries to invalidate for the entity being mutated.
Notes on invalidation
The primary goal of invalidation is to ensure that at any point in time, a load for an entity through a cache or layers of caches will return the most up-to-date value for that entity according to the source of truth stored in the database, and thus the read-through cache must be kept consistent (only current values are stored, others are invalidated).
This is done by invalidating the cache for the entity being mutated at the end of the transaction in which the mutation is performed. This ensures that the cache is invalidated as close as possible to when the source-of-truth is updated in the database, as to reduce the likelihood of collisions with loads done at the same time on other machines.
Invalidation as it pertains to transactions and nested transactions
Invalidation becomes slightly more complex when nested transactions are considered. The general guiding principle here is that over-invalidation is strictly better than under-invalidation as far as consistency goes. This is because the database is the source of truth.
For the visible-to-the-outside-world caches (cache adapters), the invalidations are done at the end of the outermost transaction (as discussed above), plus at the end of each nested transaction. While only the outermost transaction is strictly necessary for these cache adapter invalidations, the mental model of doing it at the end of each transaction, nested or otherwise, is easier to reason about.
For the dataloader caches (per-transaction local caches), the invalidation is done multiple times (over-invalidation) to better ensure that the caches are always consistent with the database as read within the transaction or nested transaction.
This over-invalidation is done because transaction isolation semantics are not consistent across all databases (some databases don't even have true nested transactions at all), meaning that whether a change made in a nested transaction is visible to the parent transaction(s) is not necessarily known. This means that the only way to ensure that the dataloader caches are consistent with the database is to invalidate them often, thus delegating consistency to the database. Invalidation of local caches is synchronous and immediate, so the performance impact of over-invalidation is negligible.
Invalidation pitfalls
One may have noticed that the above invalidation strategy still isn't perfect. Cache invalidation is hard. There still exists a very short moment in time between when invalidation occurs and when the transaction is committed, so dirty cache writes are still possible, especially in systems reading an object frequently and writing to the same object. For now, the entity framework does not attempt to provide a further solution to this problem since it is likely solutions will be case-specific. Some fun reads on the topic: