We are using EF 4.3 for our data layer and have a generic repository pattern in place. Backend is SQL 2008 R2 and the project is .NET 4.0/MVC 3 but I don’t think that this factors into the question.
Basically, we have a one to many relationship in our database of two objects. One is for ‘Traps’ and the second is for ‘Trap Activities’. Meaning, once one of these ‘Traps’ is deployed, anything that happens to that trap is kept in the Trap Activity table. Should be a fairly straightforward way of doing this.
The relationship is defined with a FK in the ‘Trap Activity’ table to the PK of the ‘Traps’ table. Both tables have PKs defined.
In our service layer, I need to query out a list of ‘Traps’ with the date that these traps were deployed. This is accomplished by the following code snippet:
var traps = this.trapRepository.Find(x => x.DeploymentYear == 2012).Select(x => new TrapHomeViewModel
{
County = x.County.Name,
DeploymentDate = x.TrapActivities.First(y => y.ActivityType == 1).ActivityDate,
State = x.County.CountyState.Abbreviation,
Latitude = x.Latitude,
Longitude = x.Longitude,
TrapId = x.TrapID,
TrapNumber = x.SerialNumber,
Centroid = x.TrapCentroid
}).ToList();
The issue is around the DeploymentDate property. As written, this takes 25s to return a list of around 3000 items. Updating the Trap table to have the deployment date to be stored there and populating with this line:
DeploymentDate = x.DeploymentDate.Value.Date
Results in a less than 1s response time. Now I think I know what is going on here (multiple enumerations of the data set) but what I thought would happen would be a query similar to the following:
SELECT Counties.Name, TrapActivities.ActivityDate, States.Abbreviation,
Traps.Latitude, Traps.Longitude, Traps.TrapID, Traps.SerialNumber, Traps.TrapCentroid
FROM TrapActivities INNER JOIN
Traps ON TrapActivities.TrapID = Traps.TrapID INNER JOIN
Counties ON Traps.CountyID = Counties.CountyID INNER JOIN
States ON Counties.State = States.FIPS_Code
WHERE (TrapActivities.ActivityType = 1)
…but that does not seem to be the case. With all the background information above, where have I strayed in populating this view model? I don’t think I have ran into this issue before but this is also a much larger dataset than some of our other projects. Any guidance on this would be much helpful. If I need to provide any other information, please let me know.
EDIT
As requested, the GenericRepository Find method and constructors:
public class GenericRepository<T> : IGenericRepository<T>
where T : class
{
private readonly IObjectSet<T> objectSet;
private ObjectContext context;
public GenericRepository()
: this(new APHISEntities())
{
}
public GenericRepository(ObjectContext context)
{
this.context = context;
this.objectSet = this.context.CreateObjectSet<T>();
}
public IEnumerable<T> Find(Func<T, bool> predicate)
{
return this.objectSet.Where(predicate);
}
EDIT 2
This is an example of the SQL being generated by the above code:
exec sp_executesql N'SELECT
[Extent1].[TrapActivityID] AS [TrapActivityID],
[Extent1].[TrapID] AS [TrapID],
[Extent1].[ActivityType] AS [ActivityType],
[Extent1].[Notes] AS [Notes],
[Extent1].[AgentID] AS [AgentID],
[Extent1].[ActivityDate] AS [ActivityDate],
[Extent1].[CreatedOn] AS [CreatedOn],
[Extent1].[EditedOn] AS [EditedOn],
[Extent1].[Deleted] AS [Deleted],
[Extent1].[VisualInspectionID] AS [VisualInspectionID]
FROM [dbo].[TrapActivities] AS [Extent1]
WHERE [Extent1].[TrapID] = @EntityKeyValue1',N'@EntityKeyValue1 uniqueidentifier',@EntityKeyValue1='FEBC7ED4-E726-4F5E-B2BA-FFD53AB7DF34'
It looks to me that it is taking a list of Trap Ids and then running a query for each one, resulting in thousands of SQL statements being generated. It also appears to be running individual queries for the County information as well.
In your repository, you can use
ObjectQuery.ToTraceStringto see what SQL is getting executed before you return your objects.You are returning all of the actual
Trapobjects deployed in 2012 from your repository find method, instead of an IQueryable AND you are not eager loadingTrapActivities. That means as you enumerate through the results inSelectto create your view model, you are sending a new query to the DB for eachTrapto get it’sTrapActivities.Update 1
I think you will need to implement a specific query in your repository for this.
Explanation
The reason your initial query was slow is because EF doesn’t eager load child relationships unless you tell it to. Lazy Loading is on by default. Since you don’t tell it to load
TrapActivitieswith yourTrapin your repository, it waits until you access it the first time to load them. This is great you need the trap but not the activities because it reduces traffic to/from the DB. However, in some situations you need them. In that case you can force an eager load by addingIncludein your query. e.g.,This loads ALL of the
TrapActivitieswith the trap in one query. However, in your case, you only need the first deployment activity which is why I created theTrapFirstDeploymentclass. This way, EF should only grab the first deployment activity.Update 2
You should also change the parameter on the
Findmethod on your repository toExpression<Func<T,Boolean>>to match theIQueryable.Wheresignature.IEnumerable.WhereusesFunc<T,Boolean>so that is whyobjectSetis getting converted to anIEnumberablebeforeWhereis called.