Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Prevent additional query when updating new graphs (faster inserts) #146

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions GraphDiff/GraphDiff.Tests/GraphDiff.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
<Compile Include="Tests\AttributeMappingBehaviours.cs" />
<Compile Include="Tests\ComplexTypeBehaviours.cs" />
<Compile Include="Tests\GuidKeyBehaviors.cs" />
<Compile Include="Tests\UpdateCollectionBehaviours.cs" />
<Compile Include="Tests\OwnedCollectionBehaviours.cs" />
<Compile Include="Tests\OneMemberBehaviours.cs" />
<Compile Include="Tests\OwnedEntityBehaviours.cs" />
Expand Down
55 changes: 55 additions & 0 deletions GraphDiff/GraphDiff.Tests/Tests/UpdateCollectionBehaviours.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System.Data.Entity;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using RefactorThis.GraphDiff.Tests.Models;

namespace RefactorThis.GraphDiff.Tests.Tests
{
[TestClass]
public class UpdateCollectionBehaviours : TestBase
{
[TestMethod]
public void ShouldAddSingleEntities()
{
var nodes = Enumerable.Range(1, 100)
.Select(i => new TestNode { Title = "Node" + i })
.ToArray();

using (var context = new TestDbContext())
{
var savedNodes = context.UpdateGraphs(nodes);
context.SaveChanges();

foreach (var node in savedNodes)
Assert.IsNotNull(context.Nodes.SingleOrDefault(p => p.Id == node.Id));
}
}

[TestMethod]
public void ShouldUpdateSingleEntities_Detached()
{
var nodes = Enumerable.Range(1, 100)
.Select(i => new TestNode { Title = "Node" + i })
.ToArray();

using (var context = new TestDbContext())
{
foreach (var node in nodes)
context.Nodes.Add(node);
context.SaveChanges();
} // Simulate detach

foreach (var node in nodes)
node.Title += "x";

using (var context = new TestDbContext())
{
context.UpdateGraphs(nodes);
context.SaveChanges();

foreach (var node in nodes)
Assert.IsTrue(context.Nodes.Single(p => p.Id == node.Id).Title.EndsWith("x"));
}
}
}
}
32 changes: 28 additions & 4 deletions GraphDiff/GraphDiff/DbContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
*/

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;
using RefactorThis.GraphDiff.Internal;
using RefactorThis.GraphDiff.Internal.Caching;
Expand Down Expand Up @@ -56,6 +58,19 @@ public static T UpdateGraph<T>(this DbContext context, T entity, UpdateParams up
return UpdateGraph(context, entity, null, null, updateParams);
}

/// <summary>
/// Merges a graph of entities with the data store.
/// </summary>
/// <typeparam name="T">The type of the root entity</typeparam>
/// <param name="context">The database context to attach / detach.</param>
/// <param name="entity">The root entities.</param>
/// <param name="updateParams">Update configuration overrides</param>
/// <returns>The attached entity graphs</returns>
public static IEnumerable<T> UpdateGraphs<T>(this DbContext context, IEnumerable<T> entities, UpdateParams updateParams = null) where T : class
{
return UpdateGraphs<T>(context, entities, null, null, updateParams);
}

/// <summary>
/// Load an aggregate type from the database (including all related entities)
/// </summary>
Expand All @@ -76,16 +91,25 @@ public static T LoadAggregate<T>(this DbContext context, Expression<Func<T, bool
}

var includeStrings = graph.GetIncludeStrings(entityManager);
return queryLoader.LoadEntity(keyPredicate, includeStrings, queryMode);
return queryLoader.LoadEntities(keyPredicate, includeStrings, queryMode).SingleOrDefault();
}

// other methods are convenience wrappers around this.
private static T UpdateGraph<T>(this DbContext context, T entity, Expression<Func<IUpdateConfiguration<T>, object>> mapping,
string mappingScheme, UpdateParams updateParams) where T : class
{
if (entity == null)
throw new ArgumentNullException("entity");

return UpdateGraphs<T>(context, new[] { entity }, mapping, mappingScheme, updateParams).First();
}

// other methods are convenience wrappers around this.
private static IEnumerable<T> UpdateGraphs<T>(this DbContext context, IEnumerable<T> entities, Expression<Func<IUpdateConfiguration<T>, object>> mapping,
string mappingScheme, UpdateParams updateParams) where T : class
{
if (entities == null)
throw new ArgumentNullException("entities");

var entityManager = new EntityManager(context);
var queryLoader = new QueryLoader(context, entityManager);
var register = new AggregateRegister(new CacheProvider());
Expand All @@ -94,7 +118,7 @@ private static T UpdateGraph<T>(this DbContext context, T entity, Expression<Fun
var differ = new GraphDiffer<T>(context, queryLoader, entityManager, root);

var queryMode = updateParams != null ? updateParams.QueryMode : QueryMode.SingleQuery;
return differ.Merge(entity, queryMode);
return differ.Merge(entities, queryMode);
}

private static GraphNode GetRootNode<T>(Expression<Func<IUpdateConfiguration<T>, object>> mapping, string mappingScheme, AggregateRegister register) where T : class
Expand All @@ -118,4 +142,4 @@ private static GraphNode GetRootNode<T>(Expression<Func<IUpdateConfiguration<T>,
return root;
}
}
}
}
12 changes: 10 additions & 2 deletions GraphDiff/GraphDiff/GraphDiffConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ public static class GraphDiffConfiguration
/// If an entity is attached as an associated entity it will be automatically reloaded from the database
/// to ensure the EF local cache has the latest state.
/// </summary>
public static bool ReloadAssociatedEntitiesWhenAttached { get; set; }
public static bool ReloadAssociatedEntitiesWhenAttached { get; set; }

/// <summary>
/// If an entity has integer primary keys (int, uint, long, ulong) with
/// empty values, it is considered to be new and not persisted.
/// In this case the loading of a persisted version of this entity can
/// be skipped to increase the performance of inserts.
/// </summary>
public static bool SkipLoadingOfNewEntities { get; set; }
}
}
}
57 changes: 38 additions & 19 deletions GraphDiff/GraphDiff/Internal/GraphDiffer.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using RefactorThis.GraphDiff.Internal.Graph;

namespace RefactorThis.GraphDiff.Internal
{
internal interface IGraphDiffer<T> where T : class
{
T Merge(T updating, QueryMode queryMode = QueryMode.SingleQuery);
IEnumerable<T> Merge(IEnumerable<T> updatingItems, QueryMode queryMode = QueryMode.SingleQuery);
}

/// <summary>GraphDiff main entry point.</summary>
Expand All @@ -26,7 +28,7 @@ public GraphDiffer(DbContext dbContext, IQueryLoader queryLoader, IEntityManager
_entityManager = entityManager;
}

public T Merge(T updating, QueryMode queryMode = QueryMode.SingleQuery)
public IEnumerable<T> Merge(IEnumerable<T> updatingItems, QueryMode queryMode = QueryMode.SingleQuery)
{
// todo query mode
bool isAutoDetectEnabled = _dbContext.Configuration.AutoDetectChangesEnabled;
Expand All @@ -37,30 +39,47 @@ public T Merge(T updating, QueryMode queryMode = QueryMode.SingleQuery)

// Get our entity with all includes needed, or add a new entity
var includeStrings = _root.GetIncludeStrings(_entityManager);
T persisted = _queryLoader.LoadEntity(updating, includeStrings, queryMode);

if (persisted == null)
var entityManager = new EntityManager(_dbContext);
var changeTracker = new ChangeTracker(_dbContext, entityManager);
var persistedItems = _queryLoader
.LoadEntities(updatingItems, includeStrings, queryMode)
.ToArray();
var index = 0;

foreach (var updating in updatingItems)
{
// we are always working with 2 graphs, simply add a 'persisted' one if none exists,
// this ensures that only the changes we make within the bounds of the mapping are attempted.
persisted = (T)_dbContext.Set(updating.GetType()).Create();
// try to get persisted entity
if (index > persistedItems.Length - 1)
{
throw new InvalidOperationException(
String.Format("Could not load all persisted entities of type '{0}'.",
typeof(T).FullName));
}

_dbContext.Set<T>().Add(persisted);
}
if (persistedItems[index] == null)
{
// we are always working with 2 graphs, simply add a 'persisted' one if none exists,
// this ensures that only the changes we make within the bounds of the mapping are attempted.
persistedItems[index] = (T)_dbContext.Set(updating.GetType()).Create();

if (_dbContext.Entry(updating).State != EntityState.Detached)
{
throw new InvalidOperationException(
_dbContext.Set<T>().Add(persistedItems[index]);
}

if (_dbContext.Entry(updating).State != EntityState.Detached)
{
throw new InvalidOperationException(
String.Format("Entity of type '{0}' is already in an attached state. GraphDiff supports detached entities only at this time. Please try AsNoTracking() or detach your entites before calling the UpdateGraph method.",
typeof (T).FullName));
}
typeof (T).FullName));
}

// Perform recursive update
var entityManager = new EntityManager(_dbContext);
var changeTracker = new ChangeTracker(_dbContext, entityManager);
_root.Update(changeTracker, entityManager, persisted, updating);
// Perform recursive update
_root.Update(changeTracker, entityManager, persistedItems[index], updating);

index++;
}

return persisted;
return persistedItems;
}
finally
{
Expand Down
Loading