From aca9d6f709db0ca9db19a9c9d2f07657c210a8d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= Date: Tue, 18 Jul 2017 23:08:46 +0200 Subject: [PATCH 1/2] NH-3488 - Anonymous selector for Linq insert/update --- doc/reference/modules/query_linq.xml | 24 ++- .../LinqBulkManipulation/Fixture.cs | 144 +++++++++++++----- src/NHibernate/Linq/DmlExpressionRewriter.cs | 73 +++++++-- src/NHibernate/Linq/InsertSyntax.cs | 12 ++ src/NHibernate/Linq/UpdateSyntax.cs | 11 ++ 5 files changed, 206 insertions(+), 58 deletions(-) diff --git a/doc/reference/modules/query_linq.xml b/doc/reference/modules/query_linq.xml index b755f619c61..c69eb3b46a6 100644 --- a/doc/reference/modules/query_linq.xml +++ b/doc/reference/modules/query_linq.xml @@ -474,7 +474,7 @@ IList oldCats = Beginning with NHibernate 5.0, Linq queries can be used for inserting, updating or deleting entities. The query defines the data to delete, update or insert, and then Delete, Update and Insert queryable extension methods allow to delete it, - or instruct in which way it should updated or inserted. Those queries happen entirely inside the + or instruct in which way it should be updated or inserted. Those queries happen entirely inside the database, without extracting corresponding entities out of the database. @@ -487,7 +487,7 @@ IList oldCats = Insert method extension expects a NHibernate queryable defining the data source of the insert. This data can be entities or a projection. Then it allows specifying the target entity type - to insert, and how to convert source data to those target entities. Two forms of target specification + to insert, and how to convert source data to those target entities. Three forms of target specification exist. @@ -497,6 +497,13 @@ IList oldCats = .Where(c => c.BodyWeight > 20) .Insert() .As(c => new Dog { Name = c.Name + "dog", BodyWeight = c.BodyWeight });]]> + + Projections can be done with an anonymous object too, but it requires supplying explicitly the target type: + + () + .Where(c => c.BodyWeight > 20) + .Insert() + .As(c => new { Name = c.Name + "dog", BodyWeight = c.BodyWeight });]]> Or using assignments: @@ -507,7 +514,7 @@ IList oldCats = .Set(d => d.Name, c => c.Name + "dog") .Set(d => d.BodyWeight, c => c.BodyWeight));]]> - In both cases, unspecified properties are not included in the resulting SQL insert. + In all cases, unspecified properties are not included in the resulting SQL insert. version and timestamp properties are exceptions. If not specified, they are inserted with their seed value. @@ -523,7 +530,7 @@ IList oldCats = Update method extension expects a queryable defining the entities to update. Then it allows specifying which properties should be updated with which values. As for - Insert, two forms of target specification exist. + Insert, three forms of target specification exist. Using projection to updated entity: @@ -532,6 +539,13 @@ IList oldCats = .Where(c => c.BodyWeight > 20) .Update() .As(c => new Cat { BodyWeight = c.BodyWeight / 2 });]]> + + Projections can be done with an anonymous object too: + + () + .Where(c => c.BodyWeight > 20) + .Update() + .As(c => new { BodyWeight = c.BodyWeight / 2 });]]> Or using assignments: @@ -541,7 +555,7 @@ IList oldCats = .Assign(a => a .Set(c => c.BodyWeight, c => c.BodyWeight / 2));]]> - In both cases, unspecified properties are not included in the resulting SQL update. This could + In all cases, unspecified properties are not included in the resulting SQL update. This could be changed for version and timestamp properties: using UpdateVersioned instead of Update allows incrementing diff --git a/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs b/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs index 74af87ff5a7..2779b85e175 100644 --- a/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs @@ -187,7 +187,20 @@ public void SimpleInsert() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query().Insert().As(x => new Pickup { Id = x.Id, Vin = x.Vin, Owner = x.Owner }); + var count = s.Query().Insert().As(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner }); + Assert.AreEqual(1, count); + + t.Commit(); + } + } + + [Test] + public void SimpleAnonymousInsert() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var count = s.Query().Insert().As(x => new { Id = -x.Id, x.Vin, x.Owner }); Assert.AreEqual(1, count); t.Commit(); @@ -200,10 +213,11 @@ public void SimpleInsertFromAggregate() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query() + var count = s + .Query() .GroupBy(x => x.Id) .Select(x => new { Id = x.Key, Vin = x.Max(y => y.Vin), Owner = x.Max(y => y.Owner) }) - .Insert().As(x => new Pickup { Id = x.Id, Vin = x.Vin, Owner = x.Owner }); + .Insert().As(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner }); Assert.AreEqual(1, count); t.Commit(); @@ -216,7 +230,8 @@ public void SimpleInsertFromLimited() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query() + var count = s + .Query() .Skip(1) .Take(1) .Insert().As(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner }); @@ -232,8 +247,9 @@ public void SimpleInsertWithConstants() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query() - .Insert().Into(x => x.Set(y => y.Id, y => y.Id).Set(y => y.Vin, y => y.Vin).Set(y => y.Owner, "The owner")); + var count = s + .Query() + .Insert().Into(x => x.Set(y => y.Id, y => -y.Id).Set(y => y.Vin, y => y.Vin).Set(y => y.Owner, "The owner")); Assert.AreEqual(1, count); t.Commit(); @@ -246,9 +262,10 @@ public void SimpleInsertFromProjection() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query() + var count = s + .Query() .Select(x => new { x.Id, x.Owner, UpperOwner = x.Owner.ToUpper() }) - .Insert().Into(x => x.Set(y => y.Id, y => y.Id).Set(y => y.Vin, y => y.UpperOwner)); + .Insert().Into(x => x.Set(y => y.Id, y => -y.Id).Set(y => y.Vin, y => y.UpperOwner)); Assert.AreEqual(1, count); t.Commit(); @@ -261,9 +278,10 @@ public void InsertWithClientSideRequirementsThrowsException() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - Assert.Throws(() => - s.Query() - .Insert().As(x => new Pickup { Id = x.Id, Vin = x.Vin, Owner = x.Owner.PadRight(200) })); + Assert.Throws( + () => s + .Query() + .Insert().As(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner.PadRight(200) })); t.Commit(); } @@ -277,7 +295,8 @@ public void InsertWithManyToOne() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query() + var count = s + .Query() .Insert().As(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight, Mother = x.Mother }); Assert.AreEqual(3, count); @@ -293,7 +312,8 @@ public void InsertWithManyToOneAsParameter() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query() + var count = s + .Query() .Insert().As(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight, Mother = _butterfly }); Assert.AreEqual(3, count); @@ -309,7 +329,8 @@ public void InsertWithManyToOneWithCompositeKey() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query() + var count = s + .Query() .Insert().As(x => new EntityReferencingEntityWithCrazyCompositeKey { Name = "Child", Parent = x }); Assert.AreEqual(1, count); @@ -324,7 +345,7 @@ public void InsertIntoSuperclassPropertiesFails() using (var t = s.BeginTransaction()) { Assert.Throws( - () => s.Query().Insert().As(x => new Human { Id = x.Id, BodyWeight = x.BodyWeight }), + () => s.Query().Insert().As(x => new Human { Id = -x.Id, BodyWeight = x.BodyWeight }), "superclass prop insertion did not error"); t.Commit(); @@ -381,10 +402,10 @@ public void InsertWithGeneratedVersionAndId() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = - s.Query() - .Where(x => x.Id == initialId) - .Insert().As(x => new IntegerVersioned { Name = x.Name, Data = x.Data }); + var count = s + .Query() + .Where(x => x.Id == initialId) + .Insert().As(x => new IntegerVersioned { Name = x.Name, Data = x.Data }); Assert.That(count, Is.EqualTo(1), "unexpected insertion count"); t.Commit(); } @@ -408,10 +429,10 @@ public void InsertWithGeneratedTimestampVersion() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = - s.Query() - .Where(x => x.Id == initialId) - .Insert().As(x => new TimestampVersioned { Name = x.Name, Data = x.Data }); + var count = s + .Query() + .Where(x => x.Id == initialId) + .Insert().As(x => new TimestampVersioned { Name = x.Name, Data = x.Data }); Assert.That(count, Is.EqualTo(1), "unexpected insertion count"); t.Commit(); @@ -438,7 +459,8 @@ public void InsertWithSelectListUsingJoins() Assert.DoesNotThrow(() => { - s.Query().Where(x => x.Mother.Mother != null) + s + .Query().Where(x => x.Mother.Mother != null) .Insert().As(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight }); }); @@ -462,13 +484,25 @@ public void InsertToComponent() // https://firebirdsql.org/file/documentation/reference_manuals/fblangref25-en/html/fblangref25-dml-insert.html#fblangref25-dml-insert-select-unstable .Where(sc => sc.Name.First != correctName) .Insert().Into(x => x.Set(y => y.Name.First, y => correctName)); - Assert.That(count, Is.EqualTo(1), "incorrect insert count"); + Assert.That(count, Is.EqualTo(1), "incorrect insert count from individual setters"); - count = - s.Query() - .Where(x => x.Name.First == correctName && x.Name.Initial != 'Z') - .Insert().As(x => new SimpleClassWithComponent { Name = new Name { First = x.Name.First, Last = x.Name.Last, Initial = 'Z' } }); - Assert.That(count, Is.EqualTo(1), "incorrect insert from corrected count"); + count = s + .Query() + .Where(x => x.Name.First == correctName && x.Name.Initial != 'Z') + .Insert().As(x => new SimpleClassWithComponent { Name = new Name { First = x.Name.First, Last = x.Name.Last, Initial = 'Z' } }); + Assert.That(count, Is.EqualTo(1), "incorrect insert from non anonymous selector"); + + count = s + .Query() + .Where(x => x.Name.First == correctName && x.Name.Initial == 'Z') + .Insert().As(x => new { Name = new { x.Name.First, x.Name.Last, Initial = 'W' } }); + Assert.That(count, Is.EqualTo(1), "incorrect insert from anonymous selector"); + + count = s + .Query() + .Where(x => x.Name.First == correctName && x.Name.Initial == 'Z') + .Insert().As(x => new { Name = new Name { First = x.Name.First, Last = x.Name.Last, Initial = 'V' } }); + Assert.That(count, Is.EqualTo(1), "incorrect insert from hybrid selector"); t.Commit(); } } @@ -488,6 +522,34 @@ private void CheckSupportOfBulkInsertionWithGeneratedId() #region UPDATES + [Test] + public void SimpleUpdate() + { + using (var s = OpenSession()) + using (s.BeginTransaction()) + { + var count = s + .Query() + .Update() + .As(a => new Car { Owner = a.Owner + " a" }); + Assert.AreEqual(1, count); + } + } + + [Test] + public void SimpleAnonymousUpdate() + { + using (var s = OpenSession()) + using (s.BeginTransaction()) + { + var count = s + .Query() + .Update() + .As(a => new { Owner = a.Owner + " a" }); + Assert.AreEqual(1, count); + } + } + [Test] public void UpdateWithWhereExistsSubquery() { @@ -501,7 +563,8 @@ public void UpdateWithWhereExistsSubquery() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query() + var count = s + .Query() .Where(x => x.Friends.OfType().Any(f => f.Name.Last == "Public")) .Update().Assign(x => x.Set(y => y.Description, "updated")); Assert.That(count, Is.EqualTo(1)); @@ -513,14 +576,16 @@ public void UpdateWithWhereExistsSubquery() using (var t = s.BeginTransaction()) { // one-to-many test - var count = s.Query() + var count = s + .Query() .Where(x => x.AssociatedEntities.Any(a => a.Name == "one-to-many-association")) .Update().Assign(x => x.Set(y => y.Name, "updated")); Assert.That(count, Is.EqualTo(1)); // many-to-many test if (Dialect.SupportsSubqueryOnMutatingTable) { - count = s.Query() + count = s + .Query() .Where(x => x.ManyToManyAssociatedEntities.Any(a => a.Name == "many-to-many-association")) .Update().Assign(x => x.Set(y => y.Name, "updated")); @@ -540,9 +605,9 @@ public void IncrementCounterVersion() using (var t = s.BeginTransaction()) { // Note: Update more than one column to showcase NH-3624, which involved losing some columns. /2014-07-26 - var count = - s.Query() - .UpdateVersioned().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd")); + var count = s + .Query() + .UpdateVersioned().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd")); Assert.That(count, Is.EqualTo(1), "incorrect exec count"); t.Commit(); } @@ -570,7 +635,8 @@ public void IncrementTimestampVersion() using (var t = s.BeginTransaction()) { // Note: Update more than one column to showcase NH-3624, which involved losing some columns. /2014-07-26 - var count = s.Query() + var count = s + .Query() .UpdateVersioned().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd")); Assert.That(count, Is.EqualTo(1), "incorrect exec count"); t.Commit(); @@ -625,8 +691,8 @@ public void UpdateWithClientSideRequirementsThrowsException() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - Assert.Throws(() => - s.Query().Where(x => x.Id == _stevee.Id).Update().As(x => new Human { Name = { First = x.Name.First.PadLeft(200) } }) + Assert.Throws( + () => s.Query().Where(x => x.Id == _stevee.Id).Update().As(x => new Human { Name = { First = x.Name.First.PadLeft(200) } }) ); t.Commit(); diff --git a/src/NHibernate/Linq/DmlExpressionRewriter.cs b/src/NHibernate/Linq/DmlExpressionRewriter.cs index 06ffc1a1953..baf8e24f2c6 100644 --- a/src/NHibernate/Linq/DmlExpressionRewriter.cs +++ b/src/NHibernate/Linq/DmlExpressionRewriter.cs @@ -25,17 +25,49 @@ public class DmlExpressionRewriter void AddSettersFromBindings(IEnumerable bindings, string path) { foreach (var node in bindings) + { + var subPath = path + "." + node.Member.Name; switch (node.BindingType) { case MemberBindingType.Assignment: - AddSettersFromAssignment((MemberAssignment) node, path + "." + node.Member.Name); + AddSettersFromAssignment((MemberAssignment)node, subPath); break; case MemberBindingType.MemberBinding: - AddSettersFromBindings(((MemberMemberBinding) node).Bindings, path + "." + node.Member.Name); + AddSettersFromBindings(((MemberMemberBinding)node).Bindings, subPath); break; default: throw new InvalidOperationException($"{node.BindingType} is not supported"); } + } + } + + void AddSettersFromAnonymousConstructor(NewExpression newExpression, string path) + { + // See Members documentation, this property is specifically designed to match constructor arguments values + // in the anonymous object case. It can be null otherwise, or non-matching. + var argumentMatchingMembers = newExpression.Members; + if (argumentMatchingMembers == null || argumentMatchingMembers.Count != newExpression.Arguments.Count) + throw new ArgumentException("The expression must be an anonymous initialization, e.g. x => new { Name = x.Name, Age = x.Age + 5 }"); + + var i = 0; + foreach (var argument in newExpression.Arguments) + { + var argumentDefinition = argumentMatchingMembers[i]; + i++; + var subPath = path + "." + argumentDefinition.Name; + switch (argument.NodeType) + { + case ExpressionType.New: + AddSettersFromAnonymousConstructor((NewExpression)argument, subPath); + break; + case ExpressionType.MemberInit: + AddSettersFromBindings(((MemberInitExpression)argument).Bindings, subPath); + break; + default: + _assignments.Add(subPath.Substring(1), Expression.Lambda(argument, _parameters)); + break; + } + } } void AddSettersFromAssignment(MemberAssignment assignment, string path) @@ -89,10 +121,32 @@ public static Expression PrepareExpression(Expression sourceEx throw new ArgumentNullException(nameof(expression)); var memberInitExpression = expression.Body as MemberInitExpression ?? - throw new ArgumentException("The expression must be member initialization, e.g. x => new Dog { Name = x.Name, Age = x.Age + 5 }"); + throw new ArgumentException("The expression must be a member initialization, e.g. x => new Dog { Name = x.Name, Age = x.Age + 5 }, " + + // If someone call InsertSyntax.As(source => new {...}), the code will fail here, so we have to hint at how to correctly + // use anonymous initialization too. + "or an anonymous initialization with an explicitly specified target type when inserting"); + + if (memberInitExpression.Type != typeof(TTarget)) + throw new TypeMismatchException($"Expecting an expression of exact type {typeof(TTarget).AssemblyQualifiedName} " + + $"but got {memberInitExpression.Type.AssemblyQualifiedName}"); - var assignments = ExtractAssignments(expression, memberInitExpression); - return PrepareExpression(sourceExpression, assignments); + var instance = new DmlExpressionRewriter(expression.Parameters); + instance.AddSettersFromBindings(memberInitExpression.Bindings, ""); + return PrepareExpression(sourceExpression, instance._assignments); + } + + public static Expression PrepareExpressionFromAnonymous(Expression sourceExpression, Expression> expression) + { + if (expression == null) + throw new ArgumentNullException(nameof(expression)); + + // Anonymous initializations are not implemented as member initialization but as plain constructor call. + var newExpression = expression.Body as NewExpression ?? + throw new ArgumentException("The expression must be an anonymous initialization, e.g. x => new { Name = x.Name, Age = x.Age + 5 }"); + + var instance = new DmlExpressionRewriter(expression.Parameters); + instance.AddSettersFromAnonymousConstructor(newExpression, ""); + return PrepareExpression(sourceExpression, instance._assignments); } public static Expression PrepareExpression(Expression sourceExpression, IReadOnlyDictionary assignments) @@ -104,14 +158,5 @@ public static Expression PrepareExpression(Expression sourceExpression, sourceExpression, Expression.Quote(lambda)); } - - static Dictionary ExtractAssignments(Expression> expression, MemberInitExpression memberInitExpression) - { - if (memberInitExpression.Type != typeof(TTarget)) - throw new TypeMismatchException($"Expecting an expression of exact type {typeof(TTarget).AssemblyQualifiedName} but got {memberInitExpression.Type.AssemblyQualifiedName}"); - var instance = new DmlExpressionRewriter(expression.Parameters); - instance.AddSettersFromBindings(memberInitExpression.Bindings, ""); - return instance._assignments; - } } } diff --git a/src/NHibernate/Linq/InsertSyntax.cs b/src/NHibernate/Linq/InsertSyntax.cs index 9f0db3f43bd..90d408075bc 100644 --- a/src/NHibernate/Linq/InsertSyntax.cs +++ b/src/NHibernate/Linq/InsertSyntax.cs @@ -45,6 +45,18 @@ public int As(Expression> expression) return ExecuteInsert(DmlExpressionRewriter.PrepareExpression(_sourceExpression, expression)); } + /// + /// Executes the insert, inserting new entities as specified by the expression. + /// + /// The type of the entities to insert. + /// The expression projecting a source entity to an anonymous object representing + /// the entity to insert. + /// The number of inserted entities. + public int As(Expression> expression) + { + return ExecuteInsert(DmlExpressionRewriter.PrepareExpressionFromAnonymous(_sourceExpression, expression)); + } + private int ExecuteInsert(Expression insertExpression) { return _provider.ExecuteDml(QueryMode.Insert, insertExpression); diff --git a/src/NHibernate/Linq/UpdateSyntax.cs b/src/NHibernate/Linq/UpdateSyntax.cs index 1ff1a99af6d..39cd3c64810 100644 --- a/src/NHibernate/Linq/UpdateSyntax.cs +++ b/src/NHibernate/Linq/UpdateSyntax.cs @@ -46,6 +46,17 @@ public int As(Expression> expression) return ExecuteUpdate(DmlExpressionRewriter.PrepareExpression(_sourceExpression, expression)); } + /// + /// Specify the assignments and execute the update. + /// + /// The assignments expressed as an anonymous object, e.g. + /// x => new { Name = x.Name, Age = x.Age + 5 }. Unset members are ignored and left untouched. + /// The number of updated entities. + public int As(Expression> expression) + { + return ExecuteUpdate(DmlExpressionRewriter.PrepareExpressionFromAnonymous(_sourceExpression, expression)); + } + private int ExecuteUpdate(Expression updateExpression) { return _provider.ExecuteDml(_versioned ? QueryMode.UpdateVersioned : QueryMode.Update, updateExpression); From 5538fb7f7a7795720f4f5fbebd00ade4faac044e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= Date: Wed, 19 Jul 2017 18:28:00 +0200 Subject: [PATCH 2/2] NH-3488 - alternate Linq DML syntax, inspired by linq2db. --- .../LinqBulkManipulation/Fixture.cs | 115 +++++++------- src/NHibernate/Linq/Assignments.cs | 12 +- src/NHibernate/Linq/DmlExtensionMethods.cs | 146 ++++++++++++++++++ src/NHibernate/Linq/InsertBuilder.cs | 82 ++++++++++ src/NHibernate/Linq/InsertSyntax.cs | 65 -------- src/NHibernate/Linq/LinqExtensionMethods.cs | 51 +----- src/NHibernate/Linq/UpdateBuilder.cs | 65 ++++++++ src/NHibernate/Linq/UpdateSyntax.cs | 65 -------- src/NHibernate/NHibernate.csproj | 5 +- 9 files changed, 362 insertions(+), 244 deletions(-) create mode 100644 src/NHibernate/Linq/DmlExtensionMethods.cs create mode 100644 src/NHibernate/Linq/InsertBuilder.cs delete mode 100644 src/NHibernate/Linq/InsertSyntax.cs create mode 100644 src/NHibernate/Linq/UpdateBuilder.cs delete mode 100644 src/NHibernate/Linq/UpdateSyntax.cs diff --git a/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs b/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs index 2779b85e175..c376163051d 100644 --- a/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs +++ b/src/NHibernate.Test/LinqBulkManipulation/Fixture.cs @@ -187,7 +187,7 @@ public void SimpleInsert() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query().Insert().As(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner }); + var count = s.Query().InsertInto(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner }); Assert.AreEqual(1, count); t.Commit(); @@ -200,7 +200,7 @@ public void SimpleAnonymousInsert() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query().Insert().As(x => new { Id = -x.Id, x.Vin, x.Owner }); + var count = s.Query().InsertInto(x => new { Id = -x.Id, x.Vin, x.Owner }); Assert.AreEqual(1, count); t.Commit(); @@ -217,7 +217,7 @@ public void SimpleInsertFromAggregate() .Query() .GroupBy(x => x.Id) .Select(x => new { Id = x.Key, Vin = x.Max(y => y.Vin), Owner = x.Max(y => y.Owner) }) - .Insert().As(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner }); + .InsertInto(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner }); Assert.AreEqual(1, count); t.Commit(); @@ -234,7 +234,7 @@ public void SimpleInsertFromLimited() .Query() .Skip(1) .Take(1) - .Insert().As(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner }); + .InsertInto(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner }); Assert.AreEqual(1, count); t.Commit(); @@ -249,7 +249,8 @@ public void SimpleInsertWithConstants() { var count = s .Query() - .Insert().Into(x => x.Set(y => y.Id, y => -y.Id).Set(y => y.Vin, y => y.Vin).Set(y => y.Owner, "The owner")); + .InsertBuilder().Into().Value(y => y.Id, y => -y.Id).Value(y => y.Vin, y => y.Vin).Value(y => y.Owner, "The owner") + .Insert(); Assert.AreEqual(1, count); t.Commit(); @@ -265,7 +266,8 @@ public void SimpleInsertFromProjection() var count = s .Query() .Select(x => new { x.Id, x.Owner, UpperOwner = x.Owner.ToUpper() }) - .Insert().Into(x => x.Set(y => y.Id, y => -y.Id).Set(y => y.Vin, y => y.UpperOwner)); + .InsertBuilder().Into().Value(y => y.Id, y => -y.Id).Value(y => y.Vin, y => y.UpperOwner) + .Insert(); Assert.AreEqual(1, count); t.Commit(); @@ -281,7 +283,7 @@ public void InsertWithClientSideRequirementsThrowsException() Assert.Throws( () => s .Query() - .Insert().As(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner.PadRight(200) })); + .InsertInto(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner.PadRight(200) })); t.Commit(); } @@ -297,7 +299,7 @@ public void InsertWithManyToOne() { var count = s .Query() - .Insert().As(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight, Mother = x.Mother }); + .InsertInto(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight, Mother = x.Mother }); Assert.AreEqual(3, count); t.Commit(); @@ -314,7 +316,7 @@ public void InsertWithManyToOneAsParameter() { var count = s .Query() - .Insert().As(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight, Mother = _butterfly }); + .InsertInto(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight, Mother = _butterfly }); Assert.AreEqual(3, count); t.Commit(); @@ -331,7 +333,7 @@ public void InsertWithManyToOneWithCompositeKey() { var count = s .Query() - .Insert().As(x => new EntityReferencingEntityWithCrazyCompositeKey { Name = "Child", Parent = x }); + .InsertInto(x => new EntityReferencingEntityWithCrazyCompositeKey { Name = "Child", Parent = x }); Assert.AreEqual(1, count); t.Commit(); @@ -345,7 +347,7 @@ public void InsertIntoSuperclassPropertiesFails() using (var t = s.BeginTransaction()) { Assert.Throws( - () => s.Query().Insert().As(x => new Human { Id = -x.Id, BodyWeight = x.BodyWeight }), + () => s.Query().InsertInto(x => new Human { Id = -x.Id, BodyWeight = x.BodyWeight }), "superclass prop insertion did not error"); t.Commit(); @@ -361,7 +363,7 @@ public void InsertAcrossMappedJoinFails() using (var t = s.BeginTransaction()) { Assert.Throws( - () => s.Query().Insert().As(x => new Joiner { Name = x.Vin, JoinedName = x.Owner }), + () => s.Query().InsertInto(x => new Joiner { Name = x.Vin, JoinedName = x.Owner }), "mapped-join insertion did not error"); t.Commit(); @@ -376,7 +378,7 @@ public void InsertWithGeneratedId() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query().Where(z => z.Id == _zoo.Id).Insert().As(x => new PettingZoo { Name = x.Name }); + var count = s.Query().Where(z => z.Id == _zoo.Id).InsertInto(x => new PettingZoo { Name = x.Name }); Assert.That(count, Is.EqualTo(1), "unexpected insertion count"); t.Commit(); } @@ -405,7 +407,7 @@ public void InsertWithGeneratedVersionAndId() var count = s .Query() .Where(x => x.Id == initialId) - .Insert().As(x => new IntegerVersioned { Name = x.Name, Data = x.Data }); + .InsertInto(x => new IntegerVersioned { Name = x.Name, Data = x.Data }); Assert.That(count, Is.EqualTo(1), "unexpected insertion count"); t.Commit(); } @@ -432,7 +434,7 @@ public void InsertWithGeneratedTimestampVersion() var count = s .Query() .Where(x => x.Id == initialId) - .Insert().As(x => new TimestampVersioned { Name = x.Name, Data = x.Data }); + .InsertInto(x => new TimestampVersioned { Name = x.Name, Data = x.Data }); Assert.That(count, Is.EqualTo(1), "unexpected insertion count"); t.Commit(); @@ -461,7 +463,7 @@ public void InsertWithSelectListUsingJoins() { s .Query().Where(x => x.Mother.Mother != null) - .Insert().As(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight }); + .InsertInto(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight }); }); s.Transaction.Commit(); @@ -483,25 +485,26 @@ public void InsertToComponent() // Avoid Firebird unstable cursor bug by filtering. // https://firebirdsql.org/file/documentation/reference_manuals/fblangref25-en/html/fblangref25-dml-insert.html#fblangref25-dml-insert-select-unstable .Where(sc => sc.Name.First != correctName) - .Insert().Into(x => x.Set(y => y.Name.First, y => correctName)); + .InsertBuilder().Into().Value(y => y.Name.First, y => correctName) + .Insert(); Assert.That(count, Is.EqualTo(1), "incorrect insert count from individual setters"); count = s .Query() .Where(x => x.Name.First == correctName && x.Name.Initial != 'Z') - .Insert().As(x => new SimpleClassWithComponent { Name = new Name { First = x.Name.First, Last = x.Name.Last, Initial = 'Z' } }); + .InsertInto(x => new SimpleClassWithComponent { Name = new Name { First = x.Name.First, Last = x.Name.Last, Initial = 'Z' } }); Assert.That(count, Is.EqualTo(1), "incorrect insert from non anonymous selector"); count = s .Query() .Where(x => x.Name.First == correctName && x.Name.Initial == 'Z') - .Insert().As(x => new { Name = new { x.Name.First, x.Name.Last, Initial = 'W' } }); + .InsertInto(x => new { Name = new { x.Name.First, x.Name.Last, Initial = 'W' } }); Assert.That(count, Is.EqualTo(1), "incorrect insert from anonymous selector"); count = s .Query() .Where(x => x.Name.First == correctName && x.Name.Initial == 'Z') - .Insert().As(x => new { Name = new Name { First = x.Name.First, Last = x.Name.Last, Initial = 'V' } }); + .InsertInto(x => new { Name = new Name { First = x.Name.First, Last = x.Name.Last, Initial = 'V' } }); Assert.That(count, Is.EqualTo(1), "incorrect insert from hybrid selector"); t.Commit(); } @@ -530,8 +533,7 @@ public void SimpleUpdate() { var count = s .Query() - .Update() - .As(a => new Car { Owner = a.Owner + " a" }); + .Update(a => new Car { Owner = a.Owner + " a" }); Assert.AreEqual(1, count); } } @@ -544,8 +546,7 @@ public void SimpleAnonymousUpdate() { var count = s .Query() - .Update() - .As(a => new { Owner = a.Owner + " a" }); + .Update(a => new { Owner = a.Owner + " a" }); Assert.AreEqual(1, count); } } @@ -566,7 +567,8 @@ public void UpdateWithWhereExistsSubquery() var count = s .Query() .Where(x => x.Friends.OfType().Any(f => f.Name.Last == "Public")) - .Update().Assign(x => x.Set(y => y.Description, "updated")); + .UpdateBuilder().Set(y => y.Description, "updated") + .Update(); Assert.That(count, Is.EqualTo(1)); t.Commit(); } @@ -579,7 +581,8 @@ public void UpdateWithWhereExistsSubquery() var count = s .Query() .Where(x => x.AssociatedEntities.Any(a => a.Name == "one-to-many-association")) - .Update().Assign(x => x.Set(y => y.Name, "updated")); + .UpdateBuilder().Set(y => y.Name, "updated") + .Update(); Assert.That(count, Is.EqualTo(1)); // many-to-many test if (Dialect.SupportsSubqueryOnMutatingTable) @@ -587,7 +590,8 @@ public void UpdateWithWhereExistsSubquery() count = s .Query() .Where(x => x.ManyToManyAssociatedEntities.Any(a => a.Name == "many-to-many-association")) - .Update().Assign(x => x.Set(y => y.Name, "updated")); + .UpdateBuilder().Set(y => y.Name, "updated") + .Update(); Assert.That(count, Is.EqualTo(1)); } @@ -607,7 +611,8 @@ public void IncrementCounterVersion() // Note: Update more than one column to showcase NH-3624, which involved losing some columns. /2014-07-26 var count = s .Query() - .UpdateVersioned().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd")); + .UpdateBuilder().Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd") + .UpdateVersioned(); Assert.That(count, Is.EqualTo(1), "incorrect exec count"); t.Commit(); } @@ -637,7 +642,8 @@ public void IncrementTimestampVersion() // Note: Update more than one column to showcase NH-3624, which involved losing some columns. /2014-07-26 var count = s .Query() - .UpdateVersioned().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd")); + .UpdateBuilder().Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd") + .UpdateVersioned(); Assert.That(count, Is.EqualTo(1), "incorrect exec count"); t.Commit(); } @@ -668,7 +674,7 @@ public void UpdateOnComponent() using (var t = s.BeginTransaction()) { var count = - s.Query().Where(x => x.Id == _stevee.Id).Update().As(x => new Human { Name = { First = correctName } }); + s.Query().Where(x => x.Id == _stevee.Id).Update(x => new Human { Name = { First = correctName } }); Assert.That(count, Is.EqualTo(1), "incorrect update count"); t.Commit(); @@ -692,7 +698,7 @@ public void UpdateWithClientSideRequirementsThrowsException() using (var t = s.BeginTransaction()) { Assert.Throws( - () => s.Query().Where(x => x.Id == _stevee.Id).Update().As(x => new Human { Name = { First = x.Name.First.PadLeft(200) } }) + () => s.Query().Where(x => x.Id == _stevee.Id).Update(x => new Human { Name = { First = x.Name.First.PadLeft(200) } }) ); t.Commit(); @@ -710,12 +716,12 @@ public void UpdateOnManyToOne() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - Assert.DoesNotThrow(() => { s.Query().Where(x => x.Id == 2).Update().Assign(x => x.Set(y => y.Mother, y => null)); }); + Assert.DoesNotThrow(() => { s.Query().Where(x => x.Id == 2).UpdateBuilder().Set(y => y.Mother, y => null).Update(); }); if (Dialect.SupportsSubqueryOnMutatingTable) { Assert.DoesNotThrow( - () => { s.Query().Where(x => x.Id == 2).Update().Assign(x => x.Set(y => y.Mother, y => s.Query().First(z => z.Id == 1))); }); + () => { s.Query().Where(x => x.Id == 2).UpdateBuilder().Set(y => y.Mother, y => s.Query().First(z => z.Id == 1)).Update(); }); } t.Commit(); @@ -729,14 +735,14 @@ public void UpdateOnDiscriminatorSubclass() { using (var t = s.BeginTransaction()) { - var count = s.Query().Update().Assign(x => x.Set(y => y.Name, y => y.Name)); + var count = s.Query().UpdateBuilder().Set(y => y.Name, y => y.Name).Update(); Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass update count"); t.Rollback(); } using (var t = s.BeginTransaction()) { - var count = s.Query().Where(x => x.Id == _pettingZoo.Id).Update().Assign(x => x.Set(y => y.Name, y => y.Name)); + var count = s.Query().Where(x => x.Id == _pettingZoo.Id).UpdateBuilder().Set(y => y.Name, y => y.Name).Update(); Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass update count"); t.Rollback(); @@ -744,7 +750,7 @@ public void UpdateOnDiscriminatorSubclass() using (var t = s.BeginTransaction()) { - var count = s.Query().Update().Assign(x => x.Set(y => y.Name, y => y.Name)); + var count = s.Query().UpdateBuilder().Set(y => y.Name, y => y.Name).Update(); Assert.That(count, Is.EqualTo(2), "Incorrect discrim subclass update count"); t.Rollback(); @@ -753,7 +759,7 @@ public void UpdateOnDiscriminatorSubclass() { // TODO : not so sure this should be allowed. Seems to me that if they specify an alias, // property-refs should be required to be qualified. - var count = s.Query().Where(x => x.Id == _zoo.Id).Update().Assign(x => x.Set(y => y.Name, y => y.Name)); + var count = s.Query().Where(x => x.Id == _zoo.Id).UpdateBuilder().Set(y => y.Name, y => y.Name).Update(); Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass update count"); t.Commit(); @@ -772,11 +778,11 @@ public void UpdateOnAnimal() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - //var count = s.Query().Where(x => x.Description == data.Frog.Description).Update(x => x.Set(y => y.Description, y => y.Description)); + //var count = s.Query().Where(x => x.Description == data.Frog.Description).Update().Set(y => y.Description, y => y.Description)); //Assert.That(count, Is.EqualTo(1), "Incorrect entity-updated count"); var count = - s.Query().Where(x => x.Description == _polliwog.Description).Update().Assign(x => x.Set(y => y.Description, y => "Tadpole")); + s.Query().Where(x => x.Description == _polliwog.Description).UpdateBuilder().Set(y => y.Description, y => "Tadpole").Update(); Assert.That(count, Is.EqualTo(1), "Incorrect entity-updated count"); var tadpole = s.Load(_polliwog.Id); @@ -784,17 +790,17 @@ public void UpdateOnAnimal() Assert.That(tadpole.Description, Is.EqualTo("Tadpole"), "Update did not take effect"); count = - s.Query().Update().Assign(x => x.Set(y => y.FireTemperature, 300)); + s.Query().UpdateBuilder().Set(y => y.FireTemperature, 300).Update(); Assert.That(count, Is.EqualTo(1), "Incorrect entity-updated count"); count = - s.Query().Update().Assign(x => x.Set(y => y.BodyWeight, y => y.BodyWeight + 1 + 1)); + s.Query().UpdateBuilder().Set(y => y.BodyWeight, y => y.BodyWeight + 1 + 1).Update(); Assert.That(count, Is.EqualTo(10), "incorrect count on 'complex' update assignment"); if (Dialect.SupportsSubqueryOnMutatingTable) { - Assert.DoesNotThrow(() => { s.Query().Update().Assign(x => x.Set(y => y.BodyWeight, y => s.Query().Max(z => z.BodyWeight))); }); + Assert.DoesNotThrow(() => { s.Query().UpdateBuilder().Set(y => y.BodyWeight, y => s.Query().Max(z => z.BodyWeight)).Update(); }); } t.Commit(); @@ -813,7 +819,7 @@ public void UpdateOnDragonWithProtectedProperty() using (var t = s.BeginTransaction()) { var count = - s.Query().Update().Assign(x => x.Set(y => y.FireTemperature, 300)); + s.Query().UpdateBuilder().Set(y => y.FireTemperature, 300).Update(); Assert.That(count, Is.EqualTo(1), "Incorrect entity-updated count"); t.Commit(); @@ -834,7 +840,7 @@ public void UpdateMultiplePropertyOnAnimal() var count = s.Query() .Where(x => x.Description == _polliwog.Description) - .Update().Assign(x => x.Set(y => y.Description, y => "Tadpole").Set(y => y.BodyWeight, 3)); + .UpdateBuilder().Set(y => y.Description, y => "Tadpole").Set(y => y.BodyWeight, 3).Update(); Assert.That(count, Is.EqualTo(1)); t.Commit(); @@ -860,16 +866,16 @@ public void UpdateOnMammal() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query().Update().Assign(x => x.Set(y => y.Description, y => y.Description)); + var count = s.Query().UpdateBuilder().Set(y => y.Description, y => y.Description).Update(); Assert.That(count, Is.EqualTo(5), "incorrect update count against 'middle' of joined-subclass hierarchy"); - count = s.Query().Update().Assign(x => x.Set(y => y.BodyWeight, 25)); + count = s.Query().UpdateBuilder().Set(y => y.BodyWeight, 25).Update(); Assert.That(count, Is.EqualTo(5), "incorrect update count against 'middle' of joined-subclass hierarchy"); if (Dialect.SupportsSubqueryOnMutatingTable) { - count = s.Query().Update().Assign(x => x.Set(y => y.BodyWeight, y => s.Query().Max(z => z.BodyWeight))); + count = s.Query().UpdateBuilder().Set(y => y.BodyWeight, y => s.Query().Max(z => z.BodyWeight)).Update(); Assert.That(count, Is.EqualTo(5), "incorrect update count against 'middle' of joined-subclass hierarchy"); } @@ -889,9 +895,9 @@ public void UpdateSetNullUnionSubclass() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query().Update().Assign(x => x.Set(y => y.Owner, "Steve")); + var count = s.Query().UpdateBuilder().Set(y => y.Owner, "Steve").Update(); Assert.That(count, Is.EqualTo(4), "incorrect restricted update count"); - count = s.Query().Where(x => x.Owner == "Steve").Update().Assign(x => x.Set(y => y.Owner, default(string))); + count = s.Query().Where(x => x.Owner == "Steve").UpdateBuilder().Set(y => y.Owner, default(string)).Update(); Assert.That(count, Is.EqualTo(4), "incorrect restricted update count"); count = s.CreateQuery("delete Vehicle where Owner is null").ExecuteUpdate(); @@ -907,13 +913,13 @@ public void UpdateSetNullOnDiscriminatorSubclass() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query().Update().Assign(x => x.Set(y => y.Address.City, default(string))); + var count = s.Query().UpdateBuilder().Set(y => y.Address.City, default(string)).Update(); Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); count = s.CreateQuery("delete Zoo where Address.City is null").ExecuteUpdate(); Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); - count = s.Query().Update().Assign(x => x.Set(y => y.Address.City, default(string))); + count = s.Query().UpdateBuilder().Set(y => y.Address.City, default(string)).Update(); Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); count = s.CreateQuery("delete Zoo where Address.City is null").ExecuteUpdate(); Assert.That(count, Is.EqualTo(1), "Incorrect discrim subclass delete count"); @@ -933,7 +939,7 @@ public void UpdateSetNullOnJoinedSubclass() using (var s = OpenSession()) using (var t = s.BeginTransaction()) { - var count = s.Query().Update().Assign(x => x.Set(y => y.BodyWeight, -1)); + var count = s.Query().UpdateBuilder().Set(y => y.BodyWeight, -1).Update(); Assert.That(count, Is.EqualTo(5), "Incorrect update count on joined subclass"); count = s.Query().Count(m => m.BodyWeight > -1.0001 && m.BodyWeight < -0.9999); @@ -950,9 +956,8 @@ public void UpdateOnOtherClassThrows() using (s.BeginTransaction()) { var query = s - .Query().Where(x => x.Mother == _butterfly) - .Update(); - Assert.That(() => query.As(a => new Human { Description = a.Description + " humanized" }), Throws.TypeOf()); + .Query().Where(x => x.Mother == _butterfly); + Assert.That(() => query.Update(a => new Human { Description = a.Description + " humanized" }), Throws.TypeOf()); } } diff --git a/src/NHibernate/Linq/Assignments.cs b/src/NHibernate/Linq/Assignments.cs index cd8ee07bebf..c84f2bf4ad3 100644 --- a/src/NHibernate/Linq/Assignments.cs +++ b/src/NHibernate/Linq/Assignments.cs @@ -11,40 +11,38 @@ namespace NHibernate.Linq /// /// The type of the entity source of the insert or to update. /// The type of the entity to insert or to update. - public class Assignments + internal class Assignments { private readonly Dictionary _assignments = new Dictionary(); internal IReadOnlyDictionary List => _assignments; /// - /// Sets the specified property. + /// Set the specified property. /// /// The type of the property. /// The property. /// The expression that should be assigned to the property. /// The current assignments list. - public Assignments Set(Expression> property, Expression> expression) + public void Set(Expression> property, Expression> expression) { if (expression == null) throw new ArgumentNullException(nameof(expression)); var member = GetMemberExpression(property); _assignments.Add(member.GetMemberPath(), expression); - return this; } /// - /// Sets the specified property. + /// Set the specified property. /// /// The type of the property. /// The property. /// The value. /// The current assignments list. - public Assignments Set(Expression> property, TProp value) + public void Set(Expression> property, TProp value) { var member = GetMemberExpression(property); _assignments.Add(member.GetMemberPath(), Expression.Constant(value, typeof(TProp))); - return this; } private static MemberExpression GetMemberExpression(Expression> property) diff --git a/src/NHibernate/Linq/DmlExtensionMethods.cs b/src/NHibernate/Linq/DmlExtensionMethods.cs new file mode 100644 index 00000000000..98d34b549eb --- /dev/null +++ b/src/NHibernate/Linq/DmlExtensionMethods.cs @@ -0,0 +1,146 @@ +using System; +using System.Linq; +using System.Linq.Expressions; + +namespace NHibernate.Linq +{ + /// + /// NHibernate LINQ DML extension methods. They are meant to work with . Supplied parameters + /// should at least have an . and + /// its overloads supply such queryables. + /// + public static class DmlExtensionMethods + { + /// + /// Delete all entities selected by the specified query. The delete operation is performed in the database without reading the entities out of it. + /// + /// The type of the elements of . + /// The query matching the entities to delete. + /// The number of deleted entities. + public static int Delete(this IQueryable source) + { + var provider = source.GetNhProvider(); + return provider.ExecuteDml(QueryMode.Delete, source.Expression); + } + + /// + /// Update all entities selected by the specified query. The update operation is performed in the database without reading the entities out of it. + /// + /// The type of the elements of . + /// The query matching the entities to update. + /// The update setters expressed as a member initialization of updated entities, e.g. + /// x => new Dog { Name = x.Name, Age = x.Age + 5 }. Unset members are ignored and left untouched. + /// The number of updated entities. + public static int Update(this IQueryable source, Expression> expression) + { + return ExecuteUpdate(source, DmlExpressionRewriter.PrepareExpression(source.Expression, expression), false); + } + + /// + /// Update all entities selected by the specified query, using an anonymous initializer for specifying setters. The update operation is performed + /// in the database without reading the entities out of it. + /// + /// The type of the elements of . + /// The query matching the entities to update. + /// The assignments expressed as an anonymous object, e.g. + /// x => new { Name = x.Name, Age = x.Age + 5 }. Unset members are ignored and left untouched. + /// The number of updated entities. + public static int Update(this IQueryable source, Expression> expression) + { + return ExecuteUpdate(source, DmlExpressionRewriter.PrepareExpressionFromAnonymous(source.Expression, expression), false); + } + + /// + /// Perform an update versioned on all entities selected by the specified query. The update operation is performed in the database without + /// reading the entities out of it. + /// + /// The type of the elements of . + /// The query matching the entities to update. + /// The update setters expressed as a member initialization of updated entities, e.g. + /// x => new Dog { Name = x.Name, Age = x.Age + 5 }. Unset members are ignored and left untouched. + /// The number of updated entities. + public static int UpdateVersioned(this IQueryable source, Expression> expression) + { + return ExecuteUpdate(source, DmlExpressionRewriter.PrepareExpression(source.Expression, expression), true); + } + + /// + /// Perform an update versioned on all entities selected by the specified query, using an anonymous initializer for specifying setters. + /// The update operation is performed in the database without reading the entities out of it. + /// + /// The type of the elements of . + /// The query matching the entities to update. + /// The assignments expressed as an anonymous object, e.g. + /// x => new { Name = x.Name, Age = x.Age + 5 }. Unset members are ignored and left untouched. + /// The number of updated entities. + public static int UpdateVersioned(this IQueryable source, Expression> expression) + { + return ExecuteUpdate(source, DmlExpressionRewriter.PrepareExpressionFromAnonymous(source.Expression, expression), true); + } + + /// + /// Initiate an update for the entities selected by the query. Return + /// a builder allowing to set properties and allowing to execute the update. + /// + /// The type of the elements of . + /// The query matching the entities to update. + /// An update builder. + public static UpdateBuilder UpdateBuilder(this IQueryable source) + { + return new UpdateBuilder(source); + } + + internal static int ExecuteUpdate(this IQueryable source, Expression updateExpression, bool versioned) + { + var provider = source.GetNhProvider(); + return provider.ExecuteDml(versioned ? QueryMode.UpdateVersioned : QueryMode.Update, updateExpression); + } + + /// + /// Insert all entities selected by the specified query. The insert operation is performed in the database without reading the entities out of it. + /// + /// The type of the elements of . + /// The type of the entities to insert. + /// The query matching entities source of the data to insert. + /// The expression projecting a source entity to the entity to insert. + /// The number of inserted entities. + public static int InsertInto(this IQueryable source, Expression> expression) + { + return ExecuteInsert(source, DmlExpressionRewriter.PrepareExpression(source.Expression, expression)); + } + + /// + /// Insert all entities selected by the specified query, using an anonymous initializer for specifying setters. + /// must be explicitly provided, e.g. source.InsertInto<Cat, Dog>(c => new {...}). The insert operation is performed in the + /// database without reading the entities out of it. + /// + /// The type of the elements of . + /// The type of the entities to insert. Must be explicitly provided. + /// The query matching entities source of the data to insert. + /// The expression projecting a source entity to an anonymous object representing + /// the entity to insert. + /// The number of inserted entities. + public static int InsertInto(this IQueryable source, Expression> expression) + { + return ExecuteInsert(source, DmlExpressionRewriter.PrepareExpressionFromAnonymous(source.Expression, expression)); + } + + /// + /// Initiate an insert using selected entities as a source. Return + /// a builder allowing to set properties to insert and allowing to execute the update. + /// + /// The type of the elements of . + /// The query matching the entities to update. + /// An update builder. + public static InsertBuilder InsertBuilder(this IQueryable source) + { + return new InsertBuilder(source); + } + + internal static int ExecuteInsert(this IQueryable source, Expression insertExpression) + { + var provider = source.GetNhProvider(); + return provider.ExecuteDml(QueryMode.Insert, insertExpression); + } + } +} diff --git a/src/NHibernate/Linq/InsertBuilder.cs b/src/NHibernate/Linq/InsertBuilder.cs new file mode 100644 index 00000000000..ac1d7b0cebb --- /dev/null +++ b/src/NHibernate/Linq/InsertBuilder.cs @@ -0,0 +1,82 @@ +using System; +using System.Linq; +using System.Linq.Expressions; + +namespace NHibernate.Linq +{ + /// + /// An insert builder on which entities to insert can be specified. + /// + /// The type of the entities selected as source of the insert. + public class InsertBuilder + { + private readonly IQueryable _source; + + internal InsertBuilder(IQueryable source) + { + _source = source; + } + + /// + /// Specifies the type of the entities to insert, and return an insert builder allowing to specify the values to insert. + /// + /// The type of the entities to insert. + /// An insert builder. + public InsertBuilder Into() + { + return new InsertBuilder(_source); + } + } + + /// + /// An insert builder on which entities to insert can be specified. + /// + /// The type of the entities selected as source of the insert. + /// The type of the entities to insert. + public class InsertBuilder + { + private readonly IQueryable _source; + private readonly Assignments _assignments = new Assignments(); + + internal InsertBuilder(IQueryable source) + { + _source = source; + } + + /// + /// Set the specified property value and return this builder. + /// + /// The type of the property. + /// The property. + /// The expression that should be assigned to the property. + /// This insert builder. + public InsertBuilder Value(Expression> property, Expression> expression) + { + _assignments.Set(property, expression); + return this; + } + + /// + /// Set the specified property value and return this builder. + /// + /// The type of the property. + /// The property. + /// The value. + /// This insert builder. + public InsertBuilder Value(Expression> property, TProp value) + { + _assignments.Set(property, value); + return this; + } + + /// + /// Insert the entities. The insert operation is performed in the database without reading the entities out of it. Will use + /// INSERT INTO [...] SELECT FROM [...] in the database. + /// + /// The number of inserted entities. + public int Insert() + { + return _source.ExecuteInsert(DmlExpressionRewriter.PrepareExpression(_source.Expression, _assignments.List)); + } + } +} \ No newline at end of file diff --git a/src/NHibernate/Linq/InsertSyntax.cs b/src/NHibernate/Linq/InsertSyntax.cs deleted file mode 100644 index 90d408075bc..00000000000 --- a/src/NHibernate/Linq/InsertSyntax.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Linq.Expressions; - -namespace NHibernate.Linq -{ - /// - /// An insert object on which entities to insert can be specified. - /// - /// The type of the entities selected as source of the insert. - public class InsertSyntax - { - private readonly Expression _sourceExpression; - private readonly INhQueryProvider _provider; - - internal InsertSyntax(Expression sourceExpression, INhQueryProvider provider) - { - _sourceExpression = sourceExpression; - _provider = provider; - } - - /// - /// Executes the insert, using the specified assignments. - /// - /// The type of the entities to insert. - /// The assignments. - /// The number of inserted entities. - public int Into(Action> assignmentActions) - { - if (assignmentActions == null) - throw new ArgumentNullException(nameof(assignmentActions)); - var assignments = new Assignments(); - assignmentActions.Invoke(assignments); - - return ExecuteInsert(DmlExpressionRewriter.PrepareExpression(_sourceExpression, assignments.List)); - } - - /// - /// Executes the insert, inserting new entities as specified by the expression. - /// - /// The type of the entities to insert. - /// The expression projecting a source entity to the entity to insert. - /// The number of inserted entities. - public int As(Expression> expression) - { - return ExecuteInsert(DmlExpressionRewriter.PrepareExpression(_sourceExpression, expression)); - } - - /// - /// Executes the insert, inserting new entities as specified by the expression. - /// - /// The type of the entities to insert. - /// The expression projecting a source entity to an anonymous object representing - /// the entity to insert. - /// The number of inserted entities. - public int As(Expression> expression) - { - return ExecuteInsert(DmlExpressionRewriter.PrepareExpressionFromAnonymous(_sourceExpression, expression)); - } - - private int ExecuteInsert(Expression insertExpression) - { - return _provider.ExecuteDml(QueryMode.Insert, insertExpression); - } - } -} \ No newline at end of file diff --git a/src/NHibernate/Linq/LinqExtensionMethods.cs b/src/NHibernate/Linq/LinqExtensionMethods.cs index 4e71b3cc408..ab8325dd5d2 100644 --- a/src/NHibernate/Linq/LinqExtensionMethods.cs +++ b/src/NHibernate/Linq/LinqExtensionMethods.cs @@ -132,61 +132,12 @@ public static IFutureValue ToFutureValue(this IQuerya return provider.ExecuteFutureValue(expression); } - /// - /// Deletes all entities selected by the specified query. The delete operation is performed in the database without reading the entities out of it. - /// - /// The type of the elements of . - /// The query matching the entities to delete. - /// The number of deleted entities. - public static int Delete(this IQueryable source) - { - var provider = GetNhProvider(source); - return provider.ExecuteDml(QueryMode.Delete, source.Expression); - } - - /// - /// Initiate an update for the entities selected by the query. The update operation will be performed in the database without reading the entities out of it. - /// - /// The type of the elements of . - /// The query matching the entities to update. - /// An update builder, allowing to specify assignments to the entities properties. - public static UpdateSyntax Update(this IQueryable source) - { - var provider = GetNhProvider(source); - return new UpdateSyntax(source.Expression, provider, false); - } - - /// - /// Initiate a update versioned for the entities selected by the query. The update operation - /// will be performed in the database without reading the entities out of it. - /// - /// The type of the elements of . - /// The query matching the entities to update. - /// An update builder, allowing to specify assignments to the entities properties. - public static UpdateSyntax UpdateVersioned(this IQueryable source) - { - var provider = GetNhProvider(source); - return new UpdateSyntax(source.Expression, provider, true); - } - - /// - /// Initiate an insert using selected entities as a source. Will use INSERT INTO [...] SELECT FROM [...] in the database. - /// - /// The type of the elements of . - /// The query matching entities source of the data to insert. - /// An insert builder, allowing to specify target entity class and assignments to its properties. - public static InsertSyntax Insert(this IQueryable source) - { - var provider = GetNhProvider(source); - return new InsertSyntax(source.Expression, provider); - } - public static T MappedAs(this T parameter, IType type) { throw new InvalidOperationException("The method should be used inside Linq to indicate a type of a parameter"); } - private static INhQueryProvider GetNhProvider(IQueryable source) + internal static INhQueryProvider GetNhProvider(this IQueryable source) { if (source == null) { diff --git a/src/NHibernate/Linq/UpdateBuilder.cs b/src/NHibernate/Linq/UpdateBuilder.cs new file mode 100644 index 00000000000..700fe32cb0d --- /dev/null +++ b/src/NHibernate/Linq/UpdateBuilder.cs @@ -0,0 +1,65 @@ +using System; +using System.Linq; +using System.Linq.Expressions; + +namespace NHibernate.Linq +{ + /// + /// An update builder on which values to update can be specified. + /// + /// The type of the entities to update. + public class UpdateBuilder + { + private readonly IQueryable _source; + private readonly Assignments _assignments = new Assignments(); + + internal UpdateBuilder(IQueryable source) + { + _source = source; + } + + /// + /// Set the specified property and return this builder. + /// + /// The type of the property. + /// The property. + /// The expression that should be assigned to the property. + /// This update builder. + public UpdateBuilder Set(Expression> property, Expression> expression) + { + _assignments.Set(property, expression); + return this; + } + + /// + /// Set the specified property and return this builder. + /// + /// The type of the property. + /// The property. + /// The value. + /// This update builder. + public UpdateBuilder Set(Expression> property, TProp value) + { + _assignments.Set(property, value); + return this; + } + + /// + /// Update the entities. The update operation is performed in the database without reading the entities out of it. + /// + /// The number of updated entities. + public int Update() + { + return _source.ExecuteUpdate(DmlExpressionRewriter.PrepareExpression(_source.Expression, _assignments.List), false); + } + + /// + /// Perform an update versioned on the entities. The update operation is performed in the database without reading the entities out of it. + /// + /// The number of updated entities. + public int UpdateVersioned() + { + return _source.ExecuteUpdate(DmlExpressionRewriter.PrepareExpression(_source.Expression, _assignments.List), true); + } + } +} \ No newline at end of file diff --git a/src/NHibernate/Linq/UpdateSyntax.cs b/src/NHibernate/Linq/UpdateSyntax.cs deleted file mode 100644 index 39cd3c64810..00000000000 --- a/src/NHibernate/Linq/UpdateSyntax.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Linq.Expressions; - -namespace NHibernate.Linq -{ - /// - /// An update object on which values to update can be specified. - /// - /// The type of the entities to update. - public class UpdateSyntax - { - private readonly Expression _sourceExpression; - private readonly INhQueryProvider _provider; - private readonly bool _versioned; - - internal UpdateSyntax(Expression sourceExpression, INhQueryProvider provider, bool versioned) - { - _sourceExpression = sourceExpression; - _provider = provider; - _versioned = versioned; - } - - /// - /// Specify the assignments and execute the update. - /// - /// The assignments. - /// The number of updated entities. - public int Assign(Action> assignmentActions) - { - if (assignmentActions == null) - throw new ArgumentNullException(nameof(assignmentActions)); - var assignments = new Assignments(); - assignmentActions.Invoke(assignments); - - return ExecuteUpdate(DmlExpressionRewriter.PrepareExpression(_sourceExpression, assignments.List)); - } - - /// - /// Specify the assignments and execute the update. - /// - /// The assignments expressed as a member initialization, e.g. - /// x => new Dog { Name = x.Name, Age = x.Age + 5 }. Unset members are ignored and left untouched. - /// The number of updated entities. - public int As(Expression> expression) - { - return ExecuteUpdate(DmlExpressionRewriter.PrepareExpression(_sourceExpression, expression)); - } - - /// - /// Specify the assignments and execute the update. - /// - /// The assignments expressed as an anonymous object, e.g. - /// x => new { Name = x.Name, Age = x.Age + 5 }. Unset members are ignored and left untouched. - /// The number of updated entities. - public int As(Expression> expression) - { - return ExecuteUpdate(DmlExpressionRewriter.PrepareExpressionFromAnonymous(_sourceExpression, expression)); - } - - private int ExecuteUpdate(Expression updateExpression) - { - return _provider.ExecuteDml(_versioned ? QueryMode.UpdateVersioned : QueryMode.Update, updateExpression); - } - } -} \ No newline at end of file diff --git a/src/NHibernate/NHibernate.csproj b/src/NHibernate/NHibernate.csproj index f4e65c70a9e..345383de14d 100644 --- a/src/NHibernate/NHibernate.csproj +++ b/src/NHibernate/NHibernate.csproj @@ -159,6 +159,7 @@ + @@ -327,7 +328,7 @@ - + @@ -338,7 +339,7 @@ - +