简体   繁体   中英

Entity Framework 6: virtual collections lazy loaded even explicitly loaded on a query

I have a problem with EF6 when trying to optimize the queries. Consider this class with one collection:

public class Client
{
    ... a lot of properties
    public virtual List<Country> Countries { get; set; }
}

As you might know, with Lazy Loading I have this n+1 problem, when EF tries to get all the Countries, for each client.

I tried to use Linq projections; for example:

        return _dbContext.Clients
            .Select(client => new
            {
                client,
                client.Countries
            }).ToList().Select(data =>
            {
                data.client.Countries = data.Countries; // Here is the problem
                return data.client;
            }).ToList();

Here I'm using two selects: the first for the Linq projection, so EF can create the SQL, and the second to map the result to a Client class. The reason for that is because I'm using a repository interface, which returns List<Client> .

Despite the query is generated with the Countries in it, EF still is using Lazy Loading when I try to render the whole information (the same n+1 problem). The only way to avoid this, is to remove the virtual accessor:

public class Client
{
    ... a lot of properties
    public List<Country> Countries { get; set; } 
}

The issue I have with this solution is that we still want to have this property as virtual. This optimization is only necessary for a particular part of the application, whilst on the other sections we want to have this Lazy Loading feature.

I don't know how to "inform" EF about this property, that has been already lazy-loaded via this Linq projection. Is that possible? If not, do we have any other options? The n+1 problems makes the application to take several seconds to load like 1000 rows.

Edit

Thanks for the responses. I know I can use the Include() extension to get the collections, but my problem is with some additional optimizations I need to add (I'm sorry for not posting the complete example, I thought with the Collection issue would be enough):

public class Client
{
    ... a lot of properties
    public virtual List<Country> Countries { get; set; }
    public virtual List<Action> Actions { get; set; }
    public virtual List<Investment> Investments { get; set; }
    public User LastUpdatedBy {
        get {
          if(Actions != null) {
              return Actions.Last();
          }       
        }
    }
}

If I need to render the clients, the information about the last update and the number of investments ( Count() ), with the Include() I practically need to bring all the information from the database. However, if I use the projection like

        return _dbContext.Clients
              .Select(client => new
              {
                    client,
                    client.Countries,
                    NumberOfInvestments = client.Investments.Count() // this is translated to an SQL query
                    LastUpdatedBy = client.Audits.OrderByDescending(m => m.Id).FirstOrDefault(),
              }).ToList().Select(data =>
              {
                    // here I map back the data
                    return data.client;
              }).ToList();

I can reduce the query, getting only the required information ( in the case of LastUpdatedBy I need to change the property to a getter/setter one, which is not a big issue, as its only used for this particular part of the application ).

If I use the Select() with this approach (projection and then mapping), the Include() section is not considered by EF.

don't know what you want to do, you are using lambda expression not linq, and your second select it's unnecessary.

data.client is client , data.Countries is client.Countries , so data.client.Countries = data.Countries alway true.

if you don't want lazy load Countries , use _dbContext.Clients.Include("Countries").Where() or select () .

In order to force eager loading of virtual properties you are supposed to use Include extension method. Here is a link to MSDN https://msdn.microsoft.com/en-us/library/jj574232(v=vs.113).aspx .

So something like this should work:

 return _dbContext.Clients.Include(c=>c.Countries).ToList();

If i understand correctly you can try this

_dbContext.LazyLoading = false;

var clientWithCountres =  _dbContext.Clients
                                    .Include(c=>c.Countries)
                                    .ToList();

This will fetch Client and only including it Countries . If you disable lazy-loading the no other collection will load from the query. Unless you are specifying a include or projection.

FYI : Projection and Include() doesn't work together see this answer If you are projection it will bypass the include. https://stackoverflow.com/a/7168225/1876572

Im not 100% sure but I think your issue is that you are still maintaining a queryable for your inner collection through to the end of the query.

This queryable is lazy (because in the model it was lazy), and you havent done anything to explain that this should not be the case, you have simply projected that same lazy queryable into the result set.

I cant tell you off the top of my head what the right answer here is but I would try things around the following:

1 use a projection on the inner queriable also eg

return _dbContext.Clients
            .Select(client => new
            {
                client,
                Countries = client.Countries.Select(c=>c)// or a new Country
            })

2 Put the include at the end of the query (Im pretty sure include applies to the result not the input. It definitally doesnt work if you put it before a projection) eg:

_dbContext.Clients
            .Select(client => new
            {
                client,
                client.Countries
            }.Include(c=>c.Countries)`

3 Try specifying the enumeration inside the projection eg:

_dbContext.Clients
            .Select(client => new
            {
                client,
                Countries = client.Countries.AsEnumerable() //perhaps tolist if it works
            }`

I do want to caviat this by saying that I havent tried any of the above but I think this will set you on the right path.

A note on lazy loading

IMO there are very few good use cases for lazy loading. It almost always causes too many queries to be generated, unless your user is following a lazy path directly on the model. Use it only with extreme caution and IMO not at all in request response (eg web) apps.

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