简体   繁体   中英

How to optimize Entity Framework query when using spatial types and automapper?

What I'm building is not something very unique. In a nutshell I'm creating a small FourSquare like service running in Azure using ASP.NET MVC 4(Web Api) and Entity Framework 5 (with Spatial support). So I'm using SQL Azure and not one of the NoSQL databases like MongoDB or CouchDB. Partly because I'm more fluent/familiar with .NET, partly to see what the development experience is (refactoring, deploying, testing) and partly to see how it will stack up against eg. node.js/MongoDB.

Now let's see some code.

/// <summary>
/// Return the nearest locations relative from the given longitude/latitude
/// </summary>
/// <param name="longitude">Longitude</param>
/// <param name="latitude">Latitude</param>
/// <param name="maxresults">Optional maximum results, default is 2</param>
/// <param name="radius">Optional maximum radius in kilometres, default is 50 km</param>
/// <returns></returns>
public JsonEnvelope Get(string longitude, string latitude, int maxresults = 2, int radius = 50)
{
    var pointTxt = string.Format("POINT({0} {1})", longitude, latitude);
    var locations = (from s in locationEntityRepository.GetAll
                     orderby s.Coordinates.Distance(DbGeography.FromText(pointTxt))
                     where s.Coordinates.Distance(DbGeography.FromText(pointTxt)) / 1000  <= radius
                     select new Location
                     {
                         Id = s.Id,
                         Name = s.Name,
                         LocationType = s.LocationType,
                         Address = s.Address,
                         Longitude = s.Coordinates.Longitude.Value,
                         Latitude = s.Coordinates.Latitude.Value,
                         Distance = (s.Coordinates.Distance(DbGeography.FromText(pointTxt)).Value) / 1000
                      })
                    .Take(maxresults).ToList();

    // Bad bad bad. But EF/Linq doesn't let us do Includes when using subqueries... Go figure
    foreach (var location in locations)
    {
        location.Checkins = AutoMapper.
                            Mapper.
                            Map<List <Checkin>, List<LocationCheckinsJsonViewModel>>
                                (checkinRepository.GetCheckinsForLocation(location.Id).ToList());
    }

    // AutoMapper.Mapper.Map<Checkin, CheckinViewModel>(dbCheckin);
    var jsonBuilder = new JsonResponseBuilder();
    jsonBuilder.AddObject2Response("locations", locations);

    return jsonBuilder.JsonEnvelope;
}

A couple of things I think I need to clarify. The locationEntityRepository.GetAll looks like this.

public IQueryable<LocationEntity> GetAll
{
    get { return _context.Locations; }
}

public IQueryable<LocationEntity> GetAllIncluding(params Expression<Func<LocationEntity, object>>[] includeProperties)
{
    IQueryable<LocationEntity> query = _context.Locations;
    foreach (var includeProperty in includeProperties) {
        query = query.Include(includeProperty);
    }

    // var tmp = query.ToList();

    return query;
}

Now the code really smells funky. Ideally I want to be able to use an GetAllIncluding(c => c.Checkins) instead of the GetAll method, and to be able to use AutoMapper to map within the LINQ projection.

I know it's by design that Include + LINQ/EF returns null by design when using subqueries. And using automapper in a LINQ/EF query should be done with the Project().To<> , but that doesn't work when using .ForMember .

So the challenge is to make the code more efficient (less SQL and easy to maintain when changes to my JSON structures are needed. Remember, we're trying to beat node.js/MongoDB here ;) Should I bother, or leave it as is?

I did something similar using the Repository pattern.

    public IEnumerable<T> FindAll()
    {
        return _context.Set<T>().ToList();
    }

    public IEnumerable<T> FindBy(Expression<Func<T, bool>> predicate)
    {
        return _context.Set<T>().Where(predicate);
    }

where the .Set method is

    public IDbSet<TEntity> Set<TEntity>() where TEntity : class
    {
        return base.Set<TEntity>();
    }

This allows you to either get everything of a type from the database or just get all of the types that meet a certain criteria.

That leaves you with a list of objects or if you use FirstOrDefault() a single object that you can map however you want.

Three things.

  1. Include the System.Data.Entity Namespace. The include Method is actually an Extension that that namespace introduces. Its even preferable to use the Lambda overload which is available in EF 4.1 and above and as such, you have to add the namespace everywhere you want to call it.
  2. Remove the .ToList() at the end of the first query as that immediately executes the query which defeats the purpose of the include.

     using System.Data.Entity public IQueryable<LocationEntity> GetAll { get { return _context.Locations; } } 

That way, you can use an Include on subqueries

Why dont you use this:

Context context=new Context();
Public List<LocationEntity> GetAll()
{
    return context.LocationEntities.Include("includeProperty").ToList();
}

And Context is this:

public class Context: DbContext
    {
        public Context()
        {
            base.Configuration.LazyLoadingEnabled = false;
            base.Configuration.ProxyCreationEnabled = false;
            base.Configuration.ValidateOnSaveEnabled = false;
        }
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
        }
        public DbSet<LocationEntity> LocationEntities{ get; set; }
    }

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