简体   繁体   中英

Lazy loading dynamic proxy POCOs n+1 select

Consider the following entity model and function:

public class Order
{
  public int OrderId {get; set;}
  public int StatusId {get; set;}

  public virtual Status OrderStatus {get; set;}
}

public class Status
{
  public int StatusId {get; set;}
  public String Name { get; set;}
}

public void ShowOrders()
{
  //load all status entities.
  //Will EF check for these in object cache first when I access order.Status for
  //the first time?
  //Or perhaps even auto include them in materialised orders?
  context.Status.Load(); 

  //enumerate orders without explicit status include
  foreach(Order o in context.Orders.ToList())
  {
    //Get Status navigation property for each order
    //Will database be hit?
    Console.WriteLine("Order: {0:N}, Status: {1}", o.OrderId, o.OrderStatus.Name);
  }
}

I know I can explicitly do:

context.Orders.Include(o=>o.OrderStatus).ToList();

to include Status when querying for orders to prevent n+1 selects. And I know that if I access Order.OrderStatus navigation property that DbReferenceEntry.IsLoaded is checked and a cached Status object retrieved if possible before database is hit.

What I'm wondering is if DbReferenceEntry.IsLoaded and DbReferenceEntry.CurrentValue are populated when the parent entity is materialised (even if .Include() is not called) if the reference entity is in the object cache already?

So in this example above when accessing Order.OrderStatus for the first time does a database query get executed even though all Status are in object cache because of the Status.Load() call prior to enumerating orders?

EF will check the object cache first whenever a navigation property is accessed, so if all the Status entities are already loaded accessing Order.OrderStatus shouldn't issue a database query. DbReferenceEntry will be populated during materialisation if the reference is already in the object cache.

If you are concerned about the number of queries being issued consider turning off Lazy Loading so in cases where an automatic query would be issued you'll get a null instead.

For more performance guidance see this article: http://msdn.microsoft.com/en-us/data/hh949853.aspx

I didn't test the following statements (by watching when and which SQL queries are actually run in a profiler), they are only guesses:

does a database query get executed even though all Status are in object cache because of the Status.Load() call prior to enumerating orders?

If a database query is not executed I'm pretty sure that the reason is not that specifically context.Status.Load() is called. This is only a snapshot of the Status database table at the time when context.Status.Load() is executed. At the point where you enumerate the orders EF can't be sure that the Status table hasn't change in the meantime and additional Status rows have been inserted. So, to avoid a wrong data representation EF must run a new query - unless EF has other means to recognize if a query is required or not.

And there are other means, in this case because an Order entity has only a reference to Status , not a Status collection. When the Order s get loaded - by enumerating context.Orders.ToList() - EF will always load the foreign key to OrderStatus , no matter if you use Include for the Status or not. It would even be the case if you hadn't exposed the FK StatusId as property in your model. At the moment when an Order is materialized, relationship fixup will run and check if a Status entity whose primary key has the same value as the FK loaded together with the Order already exists in the object context. If yes, the Order.OrderStatus property will immediately be set to that Status entity - and I guess, EF will mark the navigation property as IsLoaded . For a reference there can be only one matching entity and it wouldn't make sense to run a query if that entity is already attached to the context and assigned to the navigation property.

So, I would say: No query is run if the right Status object is attached to the context. It doesn't matter if by running context.Status.Load() or by loading only this specific Status , running any other query that would load this Status or by manually attaching the Status to the context ( context.Status.Attach(...) ).

This behaviour must change in my opinion if Order.OrderStatus would be a collection . When an Order gets loaded now there is no FK to Status . Instead the Status had an FK to Order . If you load Status entities first - by context.Status.Load or any other query - the Order-FK would be loaded with the Status objects. If you later load an Order relationship fixup would run again, this time the other way around: EF looks if there are any Status objects in the context that have an FK refering to the loaded Order . If yes, it will add the Status to the Order.OrderStatus collection. But this time - big guess - it can't mark the navigation property (the Order.OrderStatus collection) as IsLoaded because it can't be sure that really all Status objects for this Order have been loaded previously or that a new Status for that Order hasn't been added to the database in the meantime.

So, I guess, if you access the Order.OrderStatus collection lazy loading will run to ensure that the potential "rest of Status objects" for that Order gets loaded as well. Then it will mark the collection as IsLoaded . It might not need to add any additional Status to the collection, but the query is necessary at least to check if the collection was complete or not.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM