dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.72k stars 3.17k forks source link

Explicit loading for list of entries: context.Entries(...)...Load() #7350

Closed Remleo closed 2 years ago

Remleo commented 7 years ago

For explicit loading EF Core offers (from docs):

var blog = context.Blogs
        .Single(b => b.BlogId == 1);

context.Entry(blog)
        .Reference(b => b.Owner)
        .Load();

If I have, for expample, list of 10 entries, I need to 10 times call Reference(...).Load() in foreach, that generate 10 SQL queries to DB.

How about optimized method Entries():

var blogs = context.Blogs
        .Where(...)
        .ToList();

context.Entries(blogs)
        .Reference(b => b.Owner)
        .Load();

which make a single SQL query like: select .... where [BlogOwner].[BlogId] in (?, ?, ?, ?, ?, ?)

Sorry, but I have not found similar functionality. Thanks

divega commented 7 years ago

To things you can try:

divega commented 7 years ago

@Remleo BTW, we would be interested in understanding your scenario better, e.g. why didn't you use eager loading in this case.

Remleo commented 7 years ago

Sometimes may need to eager/explicit load a relationship after the parent model has already been retrieved. For example, this may be useful if I need to dynamically decide whether to load related models:

// This code is responsible for retrieve specific blog entities
// But it has no idea about inner logic in BlackBox.SomeMethod()
// That is why it dont load Navigations
var neededBlogs = context.Blogs.Where(....).ToList();
....
if (someDynamicCondition)
        BlackBox.SomeMethod(context, neededBlogs);

....

public class BlackBox {
        public static void SomeMethod(DbContext context, IEnumerable<Blog> blogs)
        {
            // There might be code that ensure Owner loading, 
            // because this method has no idea about is `blogs` was preloaded `Owners` or not.
            // Also `Loader` should be intelligent enough for load only empty Navigations
            // so calling this method multiple times is safe
            context.Entries(blogs).Reference(b => b.Owner).Load(); // dry code... my vision :)

            foreach (var blog in blogs)
            {
                if (!string.IsNullOrEmpty(blog.Owner.email) && someDynamicCondition)
                {
                    SendNotificationToOwner(blog.Owner.email, "Alert!");
                }
            }
        }
}

This code works:

var ids = context.ChangeTracker
    .Entries<Blog>()
    .Select(e => e.Property(b => b.BlogId))
    .ToList();
context.Owner.Where(o => ids.Contains(o.BlogId)).Load();

but code is not "dry". where statement need to be hardcoded and match FK for Navigation-property. Context knows all about FK so it's his "job" to load Navigation-properties properly.

Sorry for my english (

ajcvickers commented 7 years ago

Reopening so we can visit this in triage.

rowanmiller commented 7 years ago

Closing but will reconsider if we see more requests. We would consider a PR with the feature. You could also look at implementing it as an extension method.

adduss commented 7 years ago

+1

This is totally valid example for any TPT inheritance. Base model does not contains navigation properties and if we woluld like to display list with all inherited types with the common property fe. "Name", but loaded from different related entities of derivered models. We can't use include because there is no navigation property to do so, and if we load directly EF is not clever enough just to get related entites IDs by FK, but it will get whole tables, and performance will be not acceptable. In that case we have to change whole structure to TPH or create view, map it to new model and at the end we will end up with two different models to describe exactly the same entity :/

Visualisation ;): A (entity with all 3 digit numbers - like dictionary of all of them) B (base model to keep numbers assigments - abstract) --> C (derived) --> User (additional relation to entity with Name) --> D (derived) --> Company (additional relation to entity with Name) --> etc.

KieranDevvs commented 7 years ago

I would like to see this as there are many query methods that use the same query but don't use the same included references therefore, if you have a method to return a query and add all references that are used in all instances of the methods usage, then you end up with a method that works but is really inefficient. This is how I imagine it should be done?

public IQueryable<Account> GetAccountsCreatedOnDate(DateTime time) { 
    context.Account.Where(x => x.CreatedDateTime == time);
}

public IEnumerable<Post> GetPostsByUsersCreatedOnDate(DateTime time) {
    var users = GetAccountsCreatedOnDate(time);
    context.Entries(users).Reference(x => x.Posts).Load();
    return users.SelectMany(x => x.Posts);
}
smitpatel commented 7 years ago

@KieranDevlinSycous - Just try this.

public IEnumerable<Post> GetPostsByUsersCreatedOnDate(DateTime time) {
    return GetAccountsCreatedOnDate(time).SelectMany(x => x.Posts).AsEnumerable();
}
starychfojtu commented 6 years ago

I find this feature very useful. For example if you don't want to use eager loading too much, because it generates JOIN queries and if some other query already fetched the data, you are still stuck with lot of joins instead of simple selects. For example you have entity A with property C and entity B with property C and you eager load A with C and then you still have too eager load C with B instead of just selecting B. So you either have costly queries or you have many many methods on your repositories. So it would be much simpler to just load the necessary references in place. I would love this feature to be implemented. Now I use this instead

public Query<TEntity> LoadBy<TForeignEntity>(
            IEnumerable<TForeignEntity> foreignEntities,
            Func<TForeignEntity, TEntity> entitySelector,
            Func<TForeignEntity, Guid?> entityIdSelector,
            bool unrestricted = false)
        {
            var ids = foreignEntities.Where(e => entitySelector(e) == null).Select(e => entityIdSelector(e).ToOption());
            return Select(unrestricted).Where(e => e.Id, ids);
        }
romfir commented 1 year ago

Our team would really need this feature, instead of it we must manually download entries by using id of a main entry and set its collection state to be loaded (Context.Entry(obj).Collection(c => c.Collections).IsLoaded = true)