简体   繁体   中英

Best way to load navigation properties in new entity

I am trying to add new record into SQL database using EF. The code looks like

    public void Add(QueueItem queueItem)
    {
        var entity = queueItem.ApiEntity;            


        var statistic = new Statistic
        {
            Ip = entity.Ip,
            Process = entity.ProcessId,
            ApiId = entity.ApiId,
            Result = entity.Result,
            Error = entity.Error,
            Source = entity.Source,
            DateStamp = DateTime.UtcNow,
            UserId = int.Parse(entity.ApiKey),
        };

        _statisticRepository.Add(statistic);
        unitOfWork.Commit();

    }    

There is navigation Api and User properties in Statistic entity which I want to load into new Statistic entity. I have tried to load navigation properties using code below but it produce large queries and decrease performance. Any suggestion how to load navigation properties in other way?

    public Statistic Add(Statistic statistic)
    {
        _context.Statistic.Include(p => p.Api).Load();
        _context.Statistic.Include(w => w.User).Load();
        _context.Statistic.Add(statistic);
        return statistic;
    }

Some of you may have question why I want to load navigation properties while adding new entity, it's because I perform some calculations in DbContext.SaveChanges() before moving entity to database. The code looks like

public override int SaveChanges()
        {

            var addedStatistics = ChangeTracker.Entries<Statistic>().Where(e => e.State == EntityState.Added).ToList().Select(p => p.Entity).ToList();

            var userCreditsGroup = addedStatistics
                .Where(w => w.User != null)
                .GroupBy(g =>  g.User )
                .Select(s => new
                {
                    User = s.Key,
                    Count = s.Sum(p=>p.Api.CreditCost)
                })
                .ToList();      

//Skip code

}

So the Linq above will not work without loading navigation properties because it use them.

I am also adding Statistic entity for full view

  public class Statistic : Entity
    {
        public Statistic()
        {
            DateStamp = DateTime.UtcNow;

        }

        public int Id { get; set; }
        public string Process { get; set; }
        public bool Result { get; set; }
        [Required]
        public DateTime DateStamp { get; set; }

        [MaxLength(39)]
        public string Ip { get; set; }        
        [MaxLength(2083)]
        public string Source { get; set; }
        [MaxLength(250)]
        public string Error { get; set; }
        public int UserId { get; set; }
        [ForeignKey("UserId")]
        public virtual User User { get; set; }
        public int ApiId { get; set; }
        [ForeignKey("ApiId")]
        public virtual Api Api { get; set; }

    }

As you say, the following operations against your context will generate large queries:

_context.Statistic.Include(p => p.Api).Load();
_context.Statistic.Include(w => w.User).Load();

These are materialising the object graphs for all statistics and associated api entities and then all statistics and associated users into the statistics context

Just replacing this with a single call as follows will reduce this to a single round trip:

_context.Statistic.Include(p => p.Api).Include(w => w.User).Load();

Once these have been loaded, the entity framework change tracker will fixup the relationships on the new statistics entities, and hence populate the navigation properties for api and user for all new statistics in one go.

Depending on how many new statistics are being created in one go versus the number of existing statistics in the database I quite like this approach.

However, looking at the SaveChanges method it looks like the relationship fixup is happening once per new statistic. Ie each time a new statistic is added you are querying the database for all statistics and associated api and user entities to trigger a relationship fixup for the new statistic.

In which case I would be more inclined todo the following:

_context.Statistics.Add(statistic);
_context.Entry(statistic).Reference(s => s.Api).Load();
_context.Entry(statistic).Reference(s => s.User).Load();

This will only query for the Api and User of the new statistic rather than for all statistics. Ie you will generate 2 single row database queries for each new statistic.

Alternatively, if you are adding a large number of statistics in one batch, you could make use of the Local cache on the context by preloading all users and api entities upfront. Ie take the hit upfront to pre cache all user and api entities as 2 large queries.

// preload all api and user entities
_context.Apis.Load();
_context.Users.Load();

// batch add new statistics
foreach(new statistic in statisticsToAdd)
{
    statistic.User = _context.Users.Local.Single(x => x.Id == statistic.UserId);
    statistic.Api = _context.Api.Local.Single(x => x.Id == statistic.ApiId);
    _context.Statistics.Add(statistic);
}

Would be interested to find out if Entity Framework does relationship fixup from its local cache. Ie if the following would populate the navigation properties from the local cache on all the new statistics. Will have a play later.

_context.ChangeTracker.DetectChanges();

Disclaimer: all code entered directly into browser so beware of the typos.

Sorry I dont have the time to test that, but EF maps entities to objects. Therefore shouldnt simply assigning the object work:

public void Add(QueueItem queueItem)
{
    var entity = queueItem.ApiEntity;            


    var statistic = new Statistic
    {
        Ip = entity.Ip,
        Process = entity.ProcessId,
        //ApiId = entity.ApiId,
        Api = _context.Apis.Single(a => a.Id == entity.ApiId),
        Result = entity.Result,
        Error = entity.Error,
        Source = entity.Source,
        DateStamp = DateTime.UtcNow,
        //UserId = int.Parse(entity.ApiKey),
        User = _context.Users.Single(u => u.Id == int.Parse(entity.ApiKey)
    };

    _statisticRepository.Add(statistic);
    unitOfWork.Commit();

}    

I did a little guessing of your namings, you should adjust it before testing

How about make a lookup and load only necessary columns.

private readonly Dictionary<int, UserKeyType> _userKeyLookup = new Dictionary<int, UserKeyType>();

I'm not sure how you create a repository, you might need to clean up the lookup once the saving changes is completed or in the beginning of the transaction.

_userKeyLookup.Clean();

First find in the lookup, if not found then load from context.

public Statistic Add(Statistic statistic)
{
    // _context.Statistic.Include(w => w.User).Load();
    UserKeyType key;
    if (_userKeyLookup.Contains(statistic.UserId))
    {
        key = _userKeyLookup[statistic.UserId];
    }
    else
    {
        key = _context.Users.Where(u => u.Id == statistic.UserId).Select(u => u.Key).FirstOrDefault();
        _userKeyLookup.Add(statistic.UserId, key);
    }

    statistic.User = new User { Id = statistic.UserId, Key = key };

    // similar code for api..
    // _context.Statistic.Include(p => p.Api).Load();

    _context.Statistic.Add(statistic);
    return statistic;
}

Then change the grouping a little.

var userCreditsGroup = addedStatistics
    .Where(w => w.User != null)
    .GroupBy(g => g.User.Id)
    .Select(s => new
    {
        User = s.Value.First().User,
        Count = s.Sum(p=>p.Api.CreditCost)
    })
    .ToList();

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