In two previous articles I have introduced EFLazyLoading – a framework for lazy loading of entities on top of Entity Framework. In this post I will explain what stubs are and how they work.
В двух предыдущих статьях я представил вам EFLazyLoading – фреймворк для "ленивой загрузки" объектов поверх Entity Framework. В этом посте я объясню, что такое заглушки и как они работают.
Let’s establish some terminology first:
* Shell object is a public object that the users interact with. It has the properties of an entity, but no backing fields except for the primary key.
* Data object is an internal data structure that has backing fields for the object. It implements ILazyEntityDataObject interface.
* Stub object is a shell object that has no data object attached to it.
* Fully loaded object is a shell that has a data object attached and populated.
Давайте, определим терминологию:
* Shell-объект (объект-оболочка) - public объект, с которым взаимодействуют пользователи. У него есть свойства сущности, но нет никаких внутренних полей за исключением первичного ключа.
* Data-объект (объект данных) - внутренняя структура данных, которая содержит все поля сущности. Реализует интерфейс ILazyEntityDataObject.
* Stub-объект (объект-заглушка) - это объект-оболочка, который не имеет объекта данных, прикрепленного к нему.
* Полностью загруженный объект - это объект-оболочка, к которому прикрепили объект данных и загрузили последний данными из хранилища.
Here is a typical pair of shell and data objects – NorthwindEF.Category. Note a few things:Structure of the Category entity
* _CategoryID is the only field in the shell class (disregard the base class for a while). All other fields are declared in the data class
* The only public properties on the shell class are properties that correspond to EntityType definition.
* Data class is a nested type inside the shell class.
* Data class is a backing store for all non-key properties.
* Data objects must be able to deep-clone themselves. This is the purpose of ILazyEntityDataObject.Copy() method.
* Data properties (see previous article) are also declared in the Data class. This is because of pure convenience as it enables fields to be private.
* There are no public methods – only protected CreateDataObject() which takes care of creating a private data object.
Вот типичная пара объекта-оболочки и объекта данных – NorthwindEF.Category.
Отметим некоторые моменты:
* _CategoryID - единственное поле в классе оболочки (игнорируйте базовый класс некоторое время). Все другие поля объявлены в классе данных.
* Единственные public свойства в классе оболочки - свойства, которые соответствуют определению EntityType.
* Класс данных - вложенный тип в классе оболочки.
* Класс данных - вспомогательная хранилище для всех неключевых свойств.
* Объекты данных должны быть в состоянии осуществить глубокое клонирование самостоятельно. Это - цель метода ILazyEntityDataObject.Copy() метод.
* Свойства данных (см. предыдущую статью) также объявлены в классе Данных. Это сделано только ради удобства, поскольку позволяет объявить поля как private.
* Нет никаких public методов, кроме защищенного CreateDataObject(), который заботится о создании внутреннего объекта данных.
Shell objects implement ILazyEntityObject interface in addition to three IPOCO interfaces: IEntityWithKey, IEntityWithChangeTracking and IEntityWithRelationships. In current implementation those interfaces are implemented in the base class called LazyEntityObject.
Data object in current implementation it is implemented as a class with fields, but in theory it could be implemented as a hash table (to allow for types with huge number of nullable columns that are often nulls) or in some other way.
Shell-объекты осуществляют интерфейс ILazyEntityObject в дополнение к трем интерфейсам IPOCO: IEntityWithKey, IEntityWithChangeTracking и IEntityWithRelationships. В текущей реализации интерфейсы реализованы в базовом классе по имени LazyEntityObject.
Объект данных в текущей реализации реализован как класс с полями, но в теории, это может быть реализовано как хеш-таблица (чтобы учесть типы с огромным числом nullable столбцов, которые часто являются пустыми указателями), или любым другим способом.
How stubs are bornКак рождаются заглушки :)
Stubs can come to life in four possible ways:
1. Relationship navigation – when navigating a many-to-one relationship, a stub object is created to represent the related end (if the entity is not already being tracked by the ObjectStateManager).
2. IQueryable.AsStubs(). It is possible to construct a sequence of stubs (IEnumerable ) by calling AsStubs() on IQueryable . This will convert a query to a query that only projects primary keys (thus saving on database connection bandwidth).
3. IQueryable.GetStub() that returns a single result. This is a stub equivalent of calling First().
4. It is also possible to populate LazyEntityCollection with stub objects (instead of fully loaded objects) by calling LoadStubs() method.
Stubs-объекты рождаются четырьмя возможными способами:
1. При навигации по связям – при перемещении по связи "многие к одному", объект-заглушка создается, чтобы представить связанный конец (если объект еще не прослеживается ObjectStateManager).
2. IQueryable<T>.AsStubs(). Возможно создать последовательность объектов-заглушек (IEnumerable<T>), вызывая метод AsStubs() у IQueryable<T>. Это преобразует запрос в запрос, который обрабатывает только первичные ключи (таким образом сильно экономя на соединениях с базой данных).
3. IQueryable<T>.GetStub(), который возвращает единственный результат. Этот объект-заглушка эквивалентен результату вызова метода First().
4. Также возможно заполнение LazyEntityCollection объектами-заглушками (вместо полностью загруженных объектов) вызовом метода LoadStubs().
There is also a way for unmodified fully loaded object to become stub again. All you have to do is to discard their data object by calling LazyObjectContext.Reset() or by calling LazyObjectContext.ResetAllUnchangedObjects() which does the same thing for all unmodified objects in the context. This can help reduce memory footprint of your unit of work, when you are dealing with large objects and you are done processing them. Instead of detaching an object from the context, you simply discard its data – object identity is preserved and it can still be found in all relationships it belongs to, but most of objects memory can be reclaimed by GC.
Также существует способ сделать объектом-заглушкой немодифицированный, полностью загруженный, объект. Все, что вы должны сделать, отказаться от его внутреннего объекта данных, вызовом LazyObjectContext.Reset() или вызовом LazyObjectContext. ResetAllUnchangedObjects(), который делает ту же самое для всех немодифицированных объектов в контексте. Это может помочь уменьшить занимаемую память рабочего пространства, когда вы имеете дело с большими объектами, и вы закончили их обработку. Вместо того, чтобы отделить объект от контекста, вы можете просто отказаться от его данных – объектная тождественность сохраняется, и объект все еще может быть найден во всех отношениях, которым он принадлежит, но большая часть памяти объектов может быть освобождена сборщиком мусора.
Examples
Примеры
// instantiate a fully loaded entity
var prod = entities.Products.First();
// stub gets created because of relationship navigation - no load from the database here
var cat = prod.Category;
// category object gets fully loaded on first property access
Console.WriteLine("name: {0}", cat.CategoryName);
// once it is loaded we can access all properties - no database access here
Console.WriteLine("desc: {0}", cat.Description);
// iterate through details
// note that collection is populated with LoadStubs which only brings keys
// into memory
foreach (OrderDetail det in prod.OrderDetails.LoadStubs())
{
// order can be Order or InternationalOrder so it will be eagerly loaded
// because we don't know the concrete type (see below)
// next time (even in a different ObjectContext) we'll use cached type information
// so there's no server roundtrip
var order = det.Order;
Console.WriteLine("{0} {1}", det.Product.ProductName, order.OrderDate);
}
// execute a query and return collection of stub objects
var stubs = entities.Suppliers.Where(c => c.Products.Any(d=>d.Category.CategoryID == cat.CategoryID)).AsStubs();
// iterate over stubs - as we go through the collection, individual suppliers are loaded on-demand
// note how LoadStubs() is used to count Products without fully loading them
foreach (var p in stubs)
{
Console.WriteLine("Shipper {0} - {1} - {2} products", p.CompanyName, p.Phone, p.Products.LoadStubs().Count);
}
// execute a query that returns a single stub object
var singleStub = entities.Suppliers.GetStub(c=>c.SupplierID == 4);
Console.WriteLine("Stub: {0}", singleStub.Phone);
Problem with polymorphic types
Проблема с полиморфными типами
Despite our intention, we sometimes get fully loaded objects instead of stubs when calling one of the above methods – that is because of polymorphic types. For example, when your schema has a Customer base type and InternationalCustomer type derived from and there is an association from Order to Customer, you can get either a customer or international customer when you navigate the association:
We cannot possibly know the concrete type up front by examining its EntityKey. Unfortunately to create a stub/shell object we need to know the CLR type. When doing eager load, Entity Framework materializer takes care of determining concrete type by sending a specially crafted SQL query down to the server. The query includes a special discriminator column down which is used to resolve back to concrete type. Unfortunately in this case we don’t want to send any query. Even if we wanted to do that, neither Entity SQL nor LINQ have a way to project object type without loading full object, so store cannot really help us here.
Несмотря на наше намерение, мы иногда получаем полностью загруженные объекты вместо заглушек, вызывая один из вышеупомянутых методов – это может происходить из-за полиморфных типов. Например, когда у Вашей схемы есть исходный тип Customer и тип InternationalCustomer, наследованный от него и есть ассоциация Order.Client, Вы можете получить или клиента или международного клиента, когда Вы обращаетесь к Order.Client.
Мы не можем достоверно знать конкретный тип сущности, используя только ее EntityKey. К сожалению, чтобы создать заглушку/оболочку мы должны знать тип создаваемого объекта. Выполняя прямую загрузку, Entity Framework заботится об определении конкретного типа, посылая специальный SQL запрос на сервер. Запрос включает специальный столбец-дискриминатор, который используется, чтобы определить конкретный тип. К сожалению, в таком случае мы не хотим посылать никаких запросов. Даже если бы мы хотели сделать это - ни у Entity SQL, ни у LINQ нет способа создать объект нужного типа, не загружая объект полностью, так что хранилище не может реально помочь нам.
Enter IObjectTypeCacheВведение в IObjectTypeCache
IObjectTypeCache is one proposed solution to this problem. It exploits the fact, that (using normal methods) objects never change their type – there is no way to change the class of an entity stored in a database table, because Entity Framework does not allow inheritance discriminator columns (in TPH mapping) to be written to and there is no way to achieve the same thing in case of TPT or TPC mappings.
IObjectTypeCache s a cache whose keys are EntityKey objects and values are CLR types (in fact they are factory methods that return objects). This gives us amortized low cost of determining the type given a CLR type.
IObjectTypeCache - одно из решений этой проблемы. Используя тот факт, что (при использовании нормальных методов) объекты никогда не изменяют свой тип, поскольку нет никакого способа изменить класс объекта, сохраненного в таблице базы данных, потому что Entity Framework не позволяет столбцам, определяющим наследование (в TPH маппинге) записываться и нет никакого способа достигнуть той же самой вещи в случае TPT или TPC.
IObjectTypeCache - это кэш, ключи которого - EntityKey объектов, а значения являются типами CLR (фактически, они - фабричные методы возвращающие объекты). Это дает нам снизить затраты на определение конкретного типа.
Every time we create a stub (of type T), we check whether the EntityType has subclasses (defined in CSDL). If the type is known to not to be polymorphic, we just create a new instance of T.
If the type can have subclasses, we check whether the mapping from EntityKey to type is found in cache – if it is there – we just call the factory method and the stub is ready.
If the mapping is not found in the cache (which typically happens the first time a particular EntityKey is materialized in an application), we don’t try to create stubs at all – we fall back to running the fully materialized query which resolves the type for us. After this is done, we add newly discovered key-to-type mappings to our cache, so that the mappings are known next time.
There is a singleton object that holds a reference to IObjectTypeCache that all LazyObjectContexts will use – it is currently held in a static property of LazyObjectContext called ObjectTypeCache.
Каждый раз, когда мы создаем заглушку (типа T), мы проверяем, есть ли у EntityType подклассы (определенные в CSDL). Если известно, что тип не полиморфный, мы просто создаем новый экземпляр класса T.
Если у типа могут быть подклассы, мы проверяем, найдено ли EntityKey (а точнее отображение EntityKey на конкретный тип) в кэше – если это так – мы просто вызываем фабричный метод, и заглушка готова.
Если отображение не найдено в кэше (это обычно случается при первом обращении к объекту в приложении), мы не пытаемся создать заглушки вообще – мы выполняем запрос, выполняющий полную загрузку объекта, и который поможет определить конкретный тип объека. После того, как это сделано, мы добавляем недавно обнаруженные отображения EntityKey на конкретный тип, в наш кэш, так, чтобы отображения были известны в следующий раз.
Существует singleton объект, который содержит ссылку на IObjectTypeCache, который используется всеми LazyObjectContexts - он в настоящее время находятся в статическом свойстве LazyObjectContext и называется ObjectTypeCache.
WARNING: The default implementation of IObjectTypeCache (as of EFLazyLoading v0.5) does not do any cache eviction. This is typically not a problem for databases that have about one million of polymorphic objects (cache can grow up to consume 30-50MB of RAM which is usually not a problem nowadays). If your application has to scale to support more polymorphic objects than that, the sample has to be modified to add some automatic eviction (based on LRU, LFU or other strategy).
ПРЕДУПРЕЖДЕНИЕ: заданная по умолчанию реализация IObjectTypeCache (в EFLazyLoading v0.5) не делает каких-либо выталкиваний из кэша. Это, обычно, не проблема для баз данных, у которых есть около одного миллиона полиморфных объектов (кэш может расти,потребляя 30-50MB оперативной памяти, которая обычно не является проблемой в настоящее время). Если Ваше приложение будет масштабироваться, чтобы поддерживать больше полиморфных объектов, пример должен быть изменен добавлением автоматического выталкивания (основанного на LRU, LFU или другой стратегии).
Комментариев нет:
Отправить комментарий