Skip to content

Commit 1aaec80

Browse files
NH-3488 - Anonymous selector for Linq insert/update
1 parent 6aa9c18 commit 1aaec80

File tree

4 files changed

+178
-40
lines changed

4 files changed

+178
-40
lines changed

src/NHibernate.Test/LinqBulkManipulation/Fixture.cs

Lines changed: 105 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,20 @@ public void SimpleInsert()
187187
using (var s = OpenSession())
188188
using (var t = s.BeginTransaction())
189189
{
190-
var count = s.Query<Car>().Insert().As(x => new Pickup { Id = x.Id, Vin = x.Vin, Owner = x.Owner });
190+
var count = s.Query<Car>().Insert().As(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner });
191+
Assert.AreEqual(1, count);
192+
193+
t.Commit();
194+
}
195+
}
196+
197+
[Test]
198+
public void SimpleAnonymousInsert()
199+
{
200+
using (var s = OpenSession())
201+
using (var t = s.BeginTransaction())
202+
{
203+
var count = s.Query<Car>().Insert().As<Pickup>(x => new { Id = -x.Id, x.Vin, x.Owner });
191204
Assert.AreEqual(1, count);
192205

193206
t.Commit();
@@ -200,10 +213,11 @@ public void SimpleInsertFromAggregate()
200213
using (var s = OpenSession())
201214
using (var t = s.BeginTransaction())
202215
{
203-
var count = s.Query<Car>()
216+
var count = s
217+
.Query<Car>()
204218
.GroupBy(x => x.Id)
205219
.Select(x => new { Id = x.Key, Vin = x.Max(y => y.Vin), Owner = x.Max(y => y.Owner) })
206-
.Insert().As(x => new Pickup { Id = x.Id, Vin = x.Vin, Owner = x.Owner });
220+
.Insert().As(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner });
207221
Assert.AreEqual(1, count);
208222

209223
t.Commit();
@@ -216,7 +230,8 @@ public void SimpleInsertFromLimited()
216230
using (var s = OpenSession())
217231
using (var t = s.BeginTransaction())
218232
{
219-
var count = s.Query<Vehicle>()
233+
var count = s
234+
.Query<Vehicle>()
220235
.Skip(1)
221236
.Take(1)
222237
.Insert().As(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner });
@@ -232,8 +247,9 @@ public void SimpleInsertWithConstants()
232247
using (var s = OpenSession())
233248
using (var t = s.BeginTransaction())
234249
{
235-
var count = s.Query<Car>()
236-
.Insert().Into<Pickup>(x => x.Set(y => y.Id, y => y.Id).Set(y => y.Vin, y => y.Vin).Set(y => y.Owner, "The owner"));
250+
var count = s
251+
.Query<Car>()
252+
.Insert().Into<Pickup>(x => x.Set(y => y.Id, y => -y.Id).Set(y => y.Vin, y => y.Vin).Set(y => y.Owner, "The owner"));
237253
Assert.AreEqual(1, count);
238254

239255
t.Commit();
@@ -246,9 +262,10 @@ public void SimpleInsertFromProjection()
246262
using (var s = OpenSession())
247263
using (var t = s.BeginTransaction())
248264
{
249-
var count = s.Query<Car>()
265+
var count = s
266+
.Query<Car>()
250267
.Select(x => new { x.Id, x.Owner, UpperOwner = x.Owner.ToUpper() })
251-
.Insert().Into<Pickup>(x => x.Set(y => y.Id, y => y.Id).Set(y => y.Vin, y => y.UpperOwner));
268+
.Insert().Into<Pickup>(x => x.Set(y => y.Id, y => -y.Id).Set(y => y.Vin, y => y.UpperOwner));
252269
Assert.AreEqual(1, count);
253270

254271
t.Commit();
@@ -261,9 +278,10 @@ public void InsertWithClientSideRequirementsThrowsException()
261278
using (var s = OpenSession())
262279
using (var t = s.BeginTransaction())
263280
{
264-
Assert.Throws<NotSupportedException>(() =>
265-
s.Query<Car>()
266-
.Insert().As(x => new Pickup { Id = x.Id, Vin = x.Vin, Owner = x.Owner.PadRight(200) }));
281+
Assert.Throws<NotSupportedException>(
282+
() => s
283+
.Query<Car>()
284+
.Insert().As(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner.PadRight(200) }));
267285

268286
t.Commit();
269287
}
@@ -277,7 +295,8 @@ public void InsertWithManyToOne()
277295
using (var s = OpenSession())
278296
using (var t = s.BeginTransaction())
279297
{
280-
var count = s.Query<Human>()
298+
var count = s
299+
.Query<Human>()
281300
.Insert().As(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight, Mother = x.Mother });
282301
Assert.AreEqual(3, count);
283302

@@ -293,7 +312,8 @@ public void InsertWithManyToOneAsParameter()
293312
using (var s = OpenSession())
294313
using (var t = s.BeginTransaction())
295314
{
296-
var count = s.Query<Human>()
315+
var count = s
316+
.Query<Human>()
297317
.Insert().As(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight, Mother = _butterfly });
298318
Assert.AreEqual(3, count);
299319

@@ -309,7 +329,8 @@ public void InsertWithManyToOneWithCompositeKey()
309329
using (var s = OpenSession())
310330
using (var t = s.BeginTransaction())
311331
{
312-
var count = s.Query<EntityWithCrazyCompositeKey>()
332+
var count = s
333+
.Query<EntityWithCrazyCompositeKey>()
313334
.Insert().As(x => new EntityReferencingEntityWithCrazyCompositeKey { Name = "Child", Parent = x });
314335
Assert.AreEqual(1, count);
315336

@@ -324,7 +345,7 @@ public void InsertIntoSuperclassPropertiesFails()
324345
using (var t = s.BeginTransaction())
325346
{
326347
Assert.Throws<QueryException>(
327-
() => s.Query<Lizard>().Insert().As(x => new Human { Id = x.Id, BodyWeight = x.BodyWeight }),
348+
() => s.Query<Lizard>().Insert().As(x => new Human { Id = -x.Id, BodyWeight = x.BodyWeight }),
328349
"superclass prop insertion did not error");
329350

330351
t.Commit();
@@ -381,10 +402,10 @@ public void InsertWithGeneratedVersionAndId()
381402
using (var s = OpenSession())
382403
using (var t = s.BeginTransaction())
383404
{
384-
var count =
385-
s.Query<IntegerVersioned>()
386-
.Where(x => x.Id == initialId)
387-
.Insert().As(x => new IntegerVersioned { Name = x.Name, Data = x.Data });
405+
var count = s
406+
.Query<IntegerVersioned>()
407+
.Where(x => x.Id == initialId)
408+
.Insert().As(x => new IntegerVersioned { Name = x.Name, Data = x.Data });
388409
Assert.That(count, Is.EqualTo(1), "unexpected insertion count");
389410
t.Commit();
390411
}
@@ -408,10 +429,10 @@ public void InsertWithGeneratedTimestampVersion()
408429
using (var s = OpenSession())
409430
using (var t = s.BeginTransaction())
410431
{
411-
var count =
412-
s.Query<TimestampVersioned>()
413-
.Where(x => x.Id == initialId)
414-
.Insert().As(x => new TimestampVersioned { Name = x.Name, Data = x.Data });
432+
var count = s
433+
.Query<TimestampVersioned>()
434+
.Where(x => x.Id == initialId)
435+
.Insert().As(x => new TimestampVersioned { Name = x.Name, Data = x.Data });
415436
Assert.That(count, Is.EqualTo(1), "unexpected insertion count");
416437

417438
t.Commit();
@@ -438,7 +459,8 @@ public void InsertWithSelectListUsingJoins()
438459

439460
Assert.DoesNotThrow(() =>
440461
{
441-
s.Query<Human>().Where(x => x.Mother.Mother != null)
462+
s
463+
.Query<Human>().Where(x => x.Mother.Mother != null)
442464
.Insert().As(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight });
443465
});
444466

@@ -462,13 +484,25 @@ public void InsertToComponent()
462484
// https://firebirdsql.org/file/documentation/reference_manuals/fblangref25-en/html/fblangref25-dml-insert.html#fblangref25-dml-insert-select-unstable
463485
.Where(sc => sc.Name.First != correctName)
464486
.Insert().Into<SimpleClassWithComponent>(x => x.Set(y => y.Name.First, y => correctName));
465-
Assert.That(count, Is.EqualTo(1), "incorrect insert count");
487+
Assert.That(count, Is.EqualTo(1), "incorrect insert count from individual setters");
466488

467-
count =
468-
s.Query<SimpleClassWithComponent>()
469-
.Where(x => x.Name.First == correctName && x.Name.Initial != 'Z')
470-
.Insert().As(x => new SimpleClassWithComponent { Name = new Name { First = x.Name.First, Last = x.Name.Last, Initial = 'Z' } });
471-
Assert.That(count, Is.EqualTo(1), "incorrect insert from corrected count");
489+
count = s
490+
.Query<SimpleClassWithComponent>()
491+
.Where(x => x.Name.First == correctName && x.Name.Initial != 'Z')
492+
.Insert().As(x => new SimpleClassWithComponent { Name = new Name { First = x.Name.First, Last = x.Name.Last, Initial = 'Z' } });
493+
Assert.That(count, Is.EqualTo(1), "incorrect insert from non anonymous selector");
494+
495+
count = s
496+
.Query<SimpleClassWithComponent>()
497+
.Where(x => x.Name.First == correctName && x.Name.Initial == 'Z')
498+
.Insert().As<SimpleClassWithComponent>(x => new { Name = new { x.Name.First, x.Name.Last, Initial = 'W' } });
499+
Assert.That(count, Is.EqualTo(1), "incorrect insert from anonymous selector");
500+
501+
count = s
502+
.Query<SimpleClassWithComponent>()
503+
.Where(x => x.Name.First == correctName && x.Name.Initial == 'Z')
504+
.Insert().As<SimpleClassWithComponent>(x => new { Name = new Name { First = x.Name.First, Last = x.Name.Last, Initial = 'V' } });
505+
Assert.That(count, Is.EqualTo(1), "incorrect insert from hybrid selector");
472506
t.Commit();
473507
}
474508
}
@@ -488,6 +522,34 @@ private void CheckSupportOfBulkInsertionWithGeneratedId<T>()
488522

489523
#region UPDATES
490524

525+
[Test]
526+
public void SimpleUpdate()
527+
{
528+
using (var s = OpenSession())
529+
using (s.BeginTransaction())
530+
{
531+
var count = s
532+
.Query<Car>()
533+
.Update()
534+
.As(a => new Car { Owner = a.Owner + " a" });
535+
Assert.AreEqual(1, count);
536+
}
537+
}
538+
539+
[Test]
540+
public void SimpleAnonymousUpdate()
541+
{
542+
using (var s = OpenSession())
543+
using (s.BeginTransaction())
544+
{
545+
var count = s
546+
.Query<Car>()
547+
.Update()
548+
.As(a => new { Owner = a.Owner + " a" });
549+
Assert.AreEqual(1, count);
550+
}
551+
}
552+
491553
[Test]
492554
public void UpdateWithWhereExistsSubquery()
493555
{
@@ -501,7 +563,8 @@ public void UpdateWithWhereExistsSubquery()
501563
using (var s = OpenSession())
502564
using (var t = s.BeginTransaction())
503565
{
504-
var count = s.Query<Human>()
566+
var count = s
567+
.Query<Human>()
505568
.Where(x => x.Friends.OfType<Human>().Any(f => f.Name.Last == "Public"))
506569
.Update().Assign(x => x.Set(y => y.Description, "updated"));
507570
Assert.That(count, Is.EqualTo(1));
@@ -513,14 +576,16 @@ public void UpdateWithWhereExistsSubquery()
513576
using (var t = s.BeginTransaction())
514577
{
515578
// one-to-many test
516-
var count = s.Query<SimpleEntityWithAssociation>()
579+
var count = s
580+
.Query<SimpleEntityWithAssociation>()
517581
.Where(x => x.AssociatedEntities.Any(a => a.Name == "one-to-many-association"))
518582
.Update().Assign(x => x.Set(y => y.Name, "updated"));
519583
Assert.That(count, Is.EqualTo(1));
520584
// many-to-many test
521585
if (Dialect.SupportsSubqueryOnMutatingTable)
522586
{
523-
count = s.Query<SimpleEntityWithAssociation>()
587+
count = s
588+
.Query<SimpleEntityWithAssociation>()
524589
.Where(x => x.ManyToManyAssociatedEntities.Any(a => a.Name == "many-to-many-association"))
525590
.Update().Assign(x => x.Set(y => y.Name, "updated"));
526591

@@ -540,9 +605,9 @@ public void IncrementCounterVersion()
540605
using (var t = s.BeginTransaction())
541606
{
542607
// Note: Update more than one column to showcase NH-3624, which involved losing some columns. /2014-07-26
543-
var count =
544-
s.Query<IntegerVersioned>()
545-
.UpdateVersioned().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd"));
608+
var count = s
609+
.Query<IntegerVersioned>()
610+
.UpdateVersioned().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd"));
546611
Assert.That(count, Is.EqualTo(1), "incorrect exec count");
547612
t.Commit();
548613
}
@@ -570,7 +635,8 @@ public void IncrementTimestampVersion()
570635
using (var t = s.BeginTransaction())
571636
{
572637
// Note: Update more than one column to showcase NH-3624, which involved losing some columns. /2014-07-26
573-
var count = s.Query<TimestampVersioned>()
638+
var count = s
639+
.Query<TimestampVersioned>()
574640
.UpdateVersioned().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd"));
575641
Assert.That(count, Is.EqualTo(1), "incorrect exec count");
576642
t.Commit();
@@ -625,8 +691,8 @@ public void UpdateWithClientSideRequirementsThrowsException()
625691
using (var s = OpenSession())
626692
using (var t = s.BeginTransaction())
627693
{
628-
Assert.Throws<NotSupportedException>(() =>
629-
s.Query<Human>().Where(x => x.Id == _stevee.Id).Update().As(x => new Human { Name = { First = x.Name.First.PadLeft(200) } })
694+
Assert.Throws<NotSupportedException>(
695+
() => s.Query<Human>().Where(x => x.Id == _stevee.Id).Update().As(x => new Human { Name = { First = x.Name.First.PadLeft(200) } })
630696
);
631697

632698
t.Commit();

src/NHibernate/Linq/DmlExpressionRewriter.cs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,35 @@ void AddSettersFromBindings(IEnumerable<MemberBinding> bindings, string path)
3838
}
3939
}
4040

41+
void AddSettersFromConstructorArguments(NewExpression newExpression, string path)
42+
{
43+
// See Members documentation, this property is specifically designed to match constructor arguments values
44+
// in the anonymous object case. It can be null otherwise, or non-matching.
45+
var argumentMatchingMembers = newExpression.Members;
46+
if (argumentMatchingMembers == null || argumentMatchingMembers.Count != newExpression.Arguments.Count)
47+
throw new ArgumentException("The expression must be an anonymous initialization, e.g. x => new { Name = x.Name, Age = x.Age + 5 }");
48+
49+
var i = 0;
50+
foreach (var argument in newExpression.Arguments)
51+
{
52+
var argumentDefinition = argumentMatchingMembers[i];
53+
i++;
54+
var subPath = path + "." + argumentDefinition.Name;
55+
switch (argument.NodeType)
56+
{
57+
case ExpressionType.New:
58+
AddSettersFromConstructorArguments((NewExpression)argument, subPath);
59+
break;
60+
case ExpressionType.MemberInit:
61+
AddSettersFromBindings(((MemberInitExpression)argument).Bindings, subPath);
62+
break;
63+
default:
64+
_assignments.Add(subPath.Substring(1), Expression.Lambda(argument, _parameters));
65+
break;
66+
}
67+
}
68+
}
69+
4170
void AddSettersFromAssignment(MemberAssignment assignment, string path)
4271
{
4372
// {Property=new Instance{SubProperty="Value"}}
@@ -89,12 +118,25 @@ public static Expression PrepareExpression<TSource, TTarget>(Expression sourceEx
89118
throw new ArgumentNullException(nameof(expression));
90119

91120
var memberInitExpression = expression.Body as MemberInitExpression ??
92-
throw new ArgumentException("The expression must be member initialization, e.g. x => new Dog { Name = x.Name, Age = x.Age + 5 }");
121+
throw new ArgumentException("The expression must be member initialization, e.g. x => new Dog { Name = x.Name, Age = x.Age + 5 }, or an anonymous initialization with an explicitly specified target type");
93122

94123
var assignments = ExtractAssignments(expression, memberInitExpression);
95124
return PrepareExpression<TSource>(sourceExpression, assignments);
96125
}
97126

127+
public static Expression PrepareExpression<TSource>(Expression sourceExpression, Expression<Func<TSource, object>> expression)
128+
{
129+
if (expression == null)
130+
throw new ArgumentNullException(nameof(expression));
131+
132+
// Anonymous initializations are not implemented as member initialization but as plain constructor call.
133+
var newExpression = expression.Body as NewExpression ??
134+
throw new ArgumentException("The expression must be an anonymous initialization, e.g. x => new { Name = x.Name, Age = x.Age + 5 }");
135+
136+
var assignments = ExtractAssignments(expression, newExpression);
137+
return PrepareExpression<TSource>(sourceExpression, assignments);
138+
}
139+
98140
public static Expression PrepareExpression<TSource>(Expression sourceExpression, IReadOnlyDictionary<string, Expression> assignments)
99141
{
100142
var lambda = ConvertAssignmentsToDictionaryExpression<TSource>(assignments);
@@ -113,5 +155,12 @@ static Dictionary<string, Expression> ExtractAssignments<TSource, TTarget>(Expre
113155
instance.AddSettersFromBindings(memberInitExpression.Bindings, "");
114156
return instance._assignments;
115157
}
158+
159+
static Dictionary<string, Expression> ExtractAssignments<TSource>(Expression<Func<TSource, object>> expression, NewExpression newExpression)
160+
{
161+
var instance = new DmlExpressionRewriter(expression.Parameters);
162+
instance.AddSettersFromConstructorArguments(newExpression, "");
163+
return instance._assignments;
164+
}
116165
}
117166
}

src/NHibernate/Linq/InsertSyntax.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ public int As<TTarget>(Expression<Func<TSource, TTarget>> expression)
4545
return ExecuteInsert<TTarget>(DmlExpressionRewriter.PrepareExpression(_sourceExpression, expression));
4646
}
4747

48+
/// <summary>
49+
/// Executes the insert, inserting new entities as specified by the expression.
50+
/// </summary>
51+
/// <typeparam name="TTarget">The type of the entities to insert.</typeparam>
52+
/// <param name="expression">The expression projecting a source entity to an anonymous object representing
53+
/// the entity to insert.</param>
54+
/// <returns>The number of inserted entities.</returns>
55+
public int As<TTarget>(Expression<Func<TSource, object>> expression)
56+
{
57+
return ExecuteInsert<TTarget>(DmlExpressionRewriter.PrepareExpression(_sourceExpression, expression));
58+
}
59+
4860
private int ExecuteInsert<TTarget>(Expression insertExpression)
4961
{
5062
return _provider.ExecuteDml<TTarget>(QueryMode.Insert, insertExpression);

0 commit comments

Comments
 (0)