From 26f8d351c9639cb6a9644a61968db868c55efbcd Mon Sep 17 00:00:00 2001 From: Lee Timmins Date: Sun, 1 Sep 2024 18:06:39 +0100 Subject: [PATCH] Fix empty dynamic components causing a phantom update (#3600) Fix #3421 --- .../Async/NHSpecificTest/GH3421/Fixture.cs | 109 ++++++++++++++++++ .../NHSpecificTest/GH3421/Entity.cs | 21 ++++ .../NHSpecificTest/GH3421/Fixture.cs | 97 ++++++++++++++++ src/NHibernate/Async/Type/ComponentType.cs | 16 --- src/NHibernate/Type/ComponentType.cs | 16 --- 5 files changed, 227 insertions(+), 32 deletions(-) create mode 100644 src/NHibernate.Test/Async/NHSpecificTest/GH3421/Fixture.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/GH3421/Entity.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/GH3421/Fixture.cs diff --git a/src/NHibernate.Test/Async/NHSpecificTest/GH3421/Fixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/GH3421/Fixture.cs new file mode 100644 index 00000000000..ed8822a4311 --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/GH3421/Fixture.cs @@ -0,0 +1,109 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System.Collections.Generic; +using System.Linq; +using NHibernate.Cfg; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Mapping.ByCode; +using NHibernate.SqlCommand; +using NUnit.Framework; +using NHibernate.Linq; + +namespace NHibernate.Test.NHSpecificTest.GH3421 +{ + using System.Threading.Tasks; + [TestFixture] + public class ByCodeFixtureAsync : TestCaseMappingByCode + { + private SqlInterceptor _interceptor; + + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Component(x => x.Attributes, new { + Sku = (string)null + }, dc => { + dc.Property(x => x.Sku); + }); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void Configure(Configuration configuration) + { + base.Configure(configuration); + + _interceptor = new SqlInterceptor(); + + configuration.SetInterceptor(_interceptor); + } + + protected override void OnSetUp() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + var e1 = new Entity { Name = "Bob" }; + session.Save(e1); + + var e2 = new Entity { Name = "Sally", Attributes = new Dictionary() { + { "Sku", "AAA" } + } }; + session.Save(e2); + + transaction.Commit(); + } + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + session.CreateQuery("delete from Entity").ExecuteUpdate(); + transaction.Commit(); + } + } + + [Test] + public async Task TestFlushDoesNotTriggerAnUpdateAsync() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + var foo = await (session.Query().ToListAsync()); + + await (session.FlushAsync()); + + var updateStatements = _interceptor.SqlStatements.Where(s => s.ToString().ToUpper().Contains("UPDATE")).ToList(); + + Assert.That(updateStatements, Has.Count.EqualTo(0)); + } + } + + public class SqlInterceptor : EmptyInterceptor + { + public IList SqlStatements = new List(); + + public override SqlString OnPrepareStatement(SqlString sql) + { + SqlStatements.Add(sql); + + return base.OnPrepareStatement(sql); + } + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH3421/Entity.cs b/src/NHibernate.Test/NHSpecificTest/GH3421/Entity.cs new file mode 100644 index 00000000000..e208abe904c --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH3421/Entity.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; + +namespace NHibernate.Test.NHSpecificTest.GH3421 +{ + class Entity + { + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + + private IDictionary _attributes; + public virtual IDictionary Attributes { + get { + if (_attributes == null) + _attributes = new Dictionary(); + return _attributes; + } + set => _attributes = value; + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH3421/Fixture.cs b/src/NHibernate.Test/NHSpecificTest/GH3421/Fixture.cs new file mode 100644 index 00000000000..4fe7cc67954 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH3421/Fixture.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.Linq; +using NHibernate.Cfg; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Mapping.ByCode; +using NHibernate.SqlCommand; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.GH3421 +{ + [TestFixture] + public class ByCodeFixture : TestCaseMappingByCode + { + private SqlInterceptor _interceptor; + + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.Component(x => x.Attributes, new { + Sku = (string)null + }, dc => { + dc.Property(x => x.Sku); + }); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void Configure(Configuration configuration) + { + base.Configure(configuration); + + _interceptor = new SqlInterceptor(); + + configuration.SetInterceptor(_interceptor); + } + + protected override void OnSetUp() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + var e1 = new Entity { Name = "Bob" }; + session.Save(e1); + + var e2 = new Entity { Name = "Sally", Attributes = new Dictionary() { + { "Sku", "AAA" } + } }; + session.Save(e2); + + transaction.Commit(); + } + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + session.CreateQuery("delete from Entity").ExecuteUpdate(); + transaction.Commit(); + } + } + + [Test] + public void TestFlushDoesNotTriggerAnUpdate() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + var foo = session.Query().ToList(); + + session.Flush(); + + var updateStatements = _interceptor.SqlStatements.Where(s => s.ToString().ToUpper().Contains("UPDATE")).ToList(); + + Assert.That(updateStatements, Has.Count.EqualTo(0)); + } + } + + public class SqlInterceptor : EmptyInterceptor + { + public IList SqlStatements = new List(); + + public override SqlString OnPrepareStatement(SqlString sql) + { + SqlStatements.Add(sql); + + return base.OnPrepareStatement(sql); + } + } + } +} diff --git a/src/NHibernate/Async/Type/ComponentType.cs b/src/NHibernate/Async/Type/ComponentType.cs index 999852e447b..6757e22216f 100644 --- a/src/NHibernate/Async/Type/ComponentType.cs +++ b/src/NHibernate/Async/Type/ComponentType.cs @@ -33,14 +33,6 @@ public override async Task IsDirtyAsync(object x, object y, ISessionImplem { return false; } - /* - * NH Different behavior : we don't use the shortcut because NH-1101 - * let the tuplizer choose how cosiderer properties when the component is null. - */ - if (EntityMode != EntityMode.Poco && (x == null || y == null)) - { - return true; - } object[] xvalues = GetPropertyValues(x); object[] yvalues = GetPropertyValues(y); for (int i = 0; i < xvalues.Length; i++) @@ -60,14 +52,6 @@ public override async Task IsDirtyAsync(object x, object y, bool[] checkab { return false; } - /* - * NH Different behavior : we don't use the shortcut because NH-1101 - * let the tuplizer choose how cosiderer properties when the component is null. - */ - if (EntityMode != EntityMode.Poco && (x == null || y == null)) - { - return true; - } object[] xvalues = GetPropertyValues(x); object[] yvalues = GetPropertyValues(y); int loc = 0; diff --git a/src/NHibernate/Type/ComponentType.cs b/src/NHibernate/Type/ComponentType.cs index cf9cdbf552b..3c3ffe9d2d8 100644 --- a/src/NHibernate/Type/ComponentType.cs +++ b/src/NHibernate/Type/ComponentType.cs @@ -156,14 +156,6 @@ public override bool IsDirty(object x, object y, ISessionImplementor session) { return false; } - /* - * NH Different behavior : we don't use the shortcut because NH-1101 - * let the tuplizer choose how cosiderer properties when the component is null. - */ - if (EntityMode != EntityMode.Poco && (x == null || y == null)) - { - return true; - } object[] xvalues = GetPropertyValues(x); object[] yvalues = GetPropertyValues(y); for (int i = 0; i < xvalues.Length; i++) @@ -182,14 +174,6 @@ public override bool IsDirty(object x, object y, bool[] checkable, ISessionImple { return false; } - /* - * NH Different behavior : we don't use the shortcut because NH-1101 - * let the tuplizer choose how cosiderer properties when the component is null. - */ - if (EntityMode != EntityMode.Poco && (x == null || y == null)) - { - return true; - } object[] xvalues = GetPropertyValues(x); object[] yvalues = GetPropertyValues(y); int loc = 0;