diff --git a/src/NHibernate.Test/Async/NHSpecificTest/GH1235/OptionalJoinFixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/GH1235/OptionalJoinFixture.cs new file mode 100644 index 00000000000..5398f667d62 --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/GH1235/OptionalJoinFixture.cs @@ -0,0 +1,198 @@ +//------------------------------------------------------------------------------ +// +// 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.Linq; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Mapping.ByCode; +using NUnit.Framework; +using NUnit.Framework.Constraints; +using NHibernate.Linq; + +namespace NHibernate.Test.NHSpecificTest.GH1235 +{ + using System.Threading.Tasks; + //NH-2785 + [TestFixture(OptimisticLockMode.None)] + [TestFixture(OptimisticLockMode.Version)] + [TestFixture(OptimisticLockMode.Dirty)] + public class OptionalJoinFixtureAsync : TestCaseMappingByCode + { + private readonly OptimisticLockMode _optimisticLock; + + public OptionalJoinFixtureAsync(OptimisticLockMode optimisticLock) + { + _optimisticLock = optimisticLock; + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + if (Dialect.SupportsTemporaryTables) + session.CreateQuery("delete from System.Object").ExecuteUpdate(); + else + session.Delete("from System.Object"); + + transaction.Commit(); + } + } + + [Test] + public async Task UpdateNullOptionalJoinToNotNullAsync() + { + object id; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var entity = new MultiTableEntity { Name = "Bob" }; + id = await (s.SaveAsync(entity)); + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var e = await (s.GetAsync(id)); + e.OtherName = "Sally"; + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + { + var e = await (s.GetAsync(id)); + Assert.That(e.OtherName, Is.EqualTo("Sally")); + } + } + + [Test] + public async Task UpdateNullOptionalJoinToNotNullDetachedAsync() + { + object id; + MultiTableEntity entity; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + entity = new MultiTableEntity { Name = "Bob" }; + id = await (s.SaveAsync(entity)); + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + entity.OtherName = "Sally"; + await (s.UpdateAsync(entity)); + await (t.CommitAsync()); + } + + using (var s = OpenSession()) + { + var e = await (s.GetAsync(id)); + Assert.That(e.OtherName, Is.EqualTo("Sally")); + } + } + + [Test] + public async Task ShouldThrowStaleStateForOptimisticLockUpdateAsync() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var result = new MultiTableEntity { Name = "Bob", OtherName = "Bob" }; + await (s.SaveAsync(result)); + await (t.CommitAsync()); + } + + using (var s1 = OpenSession()) + using (var t1 = s1.BeginTransaction()) + { + var result = await (s1.Query().FirstOrDefaultAsync()); + + result.OtherName += "x"; + using (var s2 = OpenSession()) + { + var result2 = await (s2.Query().FirstOrDefaultAsync()); + result2.OtherName += "y"; + await (t1.CommitAsync()); + + using (var t2 = s2.BeginTransaction()) + Assert.That( + () => t2.CommitAsync(), + _optimisticLock == OptimisticLockMode.None + ? (IResolveConstraint) Throws.Nothing + : Throws.InstanceOf()); + } + } + } + + [Test] + public async Task ShouldThrowStaleStateForOptimisticLockDeleteAsync() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var result = new MultiTableEntity { Name = "Bob", OtherName = "Bob" }; + await (s.SaveAsync(result)); + await (t.CommitAsync()); + } + + using (var s1 = OpenSession()) + using (var t1 = s1.BeginTransaction()) + { + var result = await (s1.Query().FirstOrDefaultAsync()); + + result.OtherName += "x"; + using (var s2 = OpenSession()) + { + var result2 = await (s2.Query().FirstOrDefaultAsync()); + await (s2.DeleteAsync(result2)); + await (t1.CommitAsync()); + + using (var t2 = s2.BeginTransaction()) + Assert.That( + () => t2.CommitAsync(), + _optimisticLock == OptimisticLockMode.None + ? (IResolveConstraint) Throws.Nothing + : Throws.InstanceOf()); + } + } + } + + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class( + rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.Native)); + rc.DynamicUpdate(true); + rc.OptimisticLock(_optimisticLock); + + if (_optimisticLock == OptimisticLockMode.Version) + rc.Version(x => x.Version, _ => { }); + + rc.Property(x => x.Name); + rc.Join( + "SecondTable", + m => + { + m.Key(k => k.Column("Id")); + m.Property(x => x.OtherName); + m.Optional(true); + }); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH1235/Entity.cs b/src/NHibernate.Test/NHSpecificTest/GH1235/Entity.cs new file mode 100644 index 00000000000..feda2775d84 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH1235/Entity.cs @@ -0,0 +1,12 @@ +using System; + +namespace NHibernate.Test.NHSpecificTest.GH1235 +{ + class MultiTableEntity + { + public virtual int Id { get; set; } + public virtual int Version { get; set; } + public virtual string Name { get; set; } + public virtual string OtherName { get; set; } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH1235/OptionalJoinFixture.cs b/src/NHibernate.Test/NHSpecificTest/GH1235/OptionalJoinFixture.cs new file mode 100644 index 00000000000..578c6ece59d --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH1235/OptionalJoinFixture.cs @@ -0,0 +1,186 @@ +using System.Linq; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Mapping.ByCode; +using NUnit.Framework; +using NUnit.Framework.Constraints; + +namespace NHibernate.Test.NHSpecificTest.GH1235 +{ + //NH-2785 + [TestFixture(OptimisticLockMode.None)] + [TestFixture(OptimisticLockMode.Version)] + [TestFixture(OptimisticLockMode.Dirty)] + public class OptionalJoinFixture : TestCaseMappingByCode + { + private readonly OptimisticLockMode _optimisticLock; + + public OptionalJoinFixture(OptimisticLockMode optimisticLock) + { + _optimisticLock = optimisticLock; + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + if (Dialect.SupportsTemporaryTables) + session.CreateQuery("delete from System.Object").ExecuteUpdate(); + else + session.Delete("from System.Object"); + + transaction.Commit(); + } + } + + [Test] + public void UpdateNullOptionalJoinToNotNull() + { + object id; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var entity = new MultiTableEntity { Name = "Bob" }; + id = s.Save(entity); + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var e = s.Get(id); + e.OtherName = "Sally"; + t.Commit(); + } + + using (var s = OpenSession()) + { + var e = s.Get(id); + Assert.That(e.OtherName, Is.EqualTo("Sally")); + } + } + + [Test] + public void UpdateNullOptionalJoinToNotNullDetached() + { + object id; + MultiTableEntity entity; + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + entity = new MultiTableEntity { Name = "Bob" }; + id = s.Save(entity); + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + entity.OtherName = "Sally"; + s.Update(entity); + t.Commit(); + } + + using (var s = OpenSession()) + { + var e = s.Get(id); + Assert.That(e.OtherName, Is.EqualTo("Sally")); + } + } + + [Test] + public void ShouldThrowStaleStateForOptimisticLockUpdate() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var result = new MultiTableEntity { Name = "Bob", OtherName = "Bob" }; + s.Save(result); + t.Commit(); + } + + using (var s1 = OpenSession()) + using (var t1 = s1.BeginTransaction()) + { + var result = s1.Query().FirstOrDefault(); + + result.OtherName += "x"; + using (var s2 = OpenSession()) + { + var result2 = s2.Query().FirstOrDefault(); + result2.OtherName += "y"; + t1.Commit(); + + using (var t2 = s2.BeginTransaction()) + Assert.That( + () => t2.Commit(), + _optimisticLock == OptimisticLockMode.None + ? (IResolveConstraint) Throws.Nothing + : Throws.InstanceOf()); + } + } + } + + [Test] + public void ShouldThrowStaleStateForOptimisticLockDelete() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var result = new MultiTableEntity { Name = "Bob", OtherName = "Bob" }; + s.Save(result); + t.Commit(); + } + + using (var s1 = OpenSession()) + using (var t1 = s1.BeginTransaction()) + { + var result = s1.Query().FirstOrDefault(); + + result.OtherName += "x"; + using (var s2 = OpenSession()) + { + var result2 = s2.Query().FirstOrDefault(); + s2.Delete(result2); + t1.Commit(); + + using (var t2 = s2.BeginTransaction()) + Assert.That( + () => t2.Commit(), + _optimisticLock == OptimisticLockMode.None + ? (IResolveConstraint) Throws.Nothing + : Throws.InstanceOf()); + } + } + } + + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class( + rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.Native)); + rc.DynamicUpdate(true); + rc.OptimisticLock(_optimisticLock); + + if (_optimisticLock == OptimisticLockMode.Version) + rc.Version(x => x.Version, _ => { }); + + rc.Property(x => x.Name); + rc.Join( + "SecondTable", + m => + { + m.Key(k => k.Column("Id")); + m.Property(x => x.OtherName); + m.Optional(true); + }); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + } +} diff --git a/src/NHibernate/Async/Persister/Entity/AbstractEntityPersister.cs b/src/NHibernate/Async/Persister/Entity/AbstractEntityPersister.cs index d5005ea10af..1e9ec77f2ba 100644 --- a/src/NHibernate/Async/Persister/Entity/AbstractEntityPersister.cs +++ b/src/NHibernate/Async/Persister/Entity/AbstractEntityPersister.cs @@ -756,7 +756,7 @@ protected async Task UpdateAsync(object id, object[] fields, object[] oldF if (CheckVersion(includeProperty)) await (VersionType.NullSafeSetAsync(statement, oldVersion, index, session, cancellationToken)).ConfigureAwait(false); } - else if (entityMetamodel.OptimisticLockMode > Versioning.OptimisticLock.Version && oldFields != null) + else if (IsPropertyBasedOptimisticLocking(oldFields)) { bool[] versionability = PropertyVersionability; bool[] includeOldField = OptimisticLockMode == Versioning.OptimisticLock.All @@ -785,7 +785,7 @@ protected async Task UpdateAsync(object id, object[] fields, object[] oldF } else { - return Check(await (session.Batcher.ExecuteNonQueryAsync(statement, cancellationToken)).ConfigureAwait(false), id, j, expectation, statement); + return Check(await (session.Batcher.ExecuteNonQueryAsync(statement, cancellationToken)).ConfigureAwait(false), id, j, expectation, statement, IsPropertyBasedOptimisticLocking(oldFields)); } } catch (OperationCanceledException) { throw; } @@ -891,7 +891,7 @@ public async Task DeleteAsync(object id, object version, int j, object obj, SqlC { await (VersionType.NullSafeSetAsync(statement, version, index, session, cancellationToken)).ConfigureAwait(false); } - else if (entityMetamodel.OptimisticLockMode > Versioning.OptimisticLock.Version && loadedState != null) + else if (IsPropertyBasedOptimisticLocking(loadedState)) { bool[] versionability = PropertyVersionability; IType[] types = PropertyTypes; @@ -915,7 +915,7 @@ public async Task DeleteAsync(object id, object version, int j, object obj, SqlC } else { - Check(await (session.Batcher.ExecuteNonQueryAsync(statement, cancellationToken)).ConfigureAwait(false), tableId, j, expectation, statement); + Check(await (session.Batcher.ExecuteNonQueryAsync(statement, cancellationToken)).ConfigureAwait(false), tableId, j, expectation, statement, IsPropertyBasedOptimisticLocking(loadedState)); } } catch (OperationCanceledException) { throw; } diff --git a/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs b/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs index ccad078e322..fd0dd32ceb0 100644 --- a/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs +++ b/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs @@ -2516,7 +2516,13 @@ protected IUniqueEntityLoader CreateEntityLoader(LockMode lockMode) return CreateEntityLoader(lockMode, CollectionHelper.EmptyDictionary()); } + //TODO 6.0: Remove (replaced by overload with optional parameter) protected bool Check(int rows, object id, int tableNumber, IExpectation expectation, DbCommand statement) + { + return Check(rows, id, tableNumber, expectation, statement, false); + } + + protected bool Check(int rows, object id, int tableNumber, IExpectation expectation, DbCommand statement, bool forceThrowStaleException = false) { try { @@ -2524,13 +2530,15 @@ protected bool Check(int rows, object id, int tableNumber, IExpectation expectat } catch (StaleStateException sse) { - if (!IsNullableTable(tableNumber)) + if (forceThrowStaleException || !IsNullableTable(tableNumber)) { if (Factory.Statistics.IsStatisticsEnabled) Factory.StatisticsImplementor.OptimisticFailure(EntityName); throw new StaleObjectStateException(EntityName, id, sse); } + + return false; } catch (TooManyRowsAffectedException ex) { @@ -2592,7 +2600,7 @@ protected internal SqlCommandInfo GenerateUpdateString(bool[] includeProperty, i hasColumns = true; } } - else if (entityMetamodel.OptimisticLockMode > Versioning.OptimisticLock.Version && oldFields != null) + else if (IsPropertyBasedOptimisticLocking(oldFields)) { // we are using "all" or "dirty" property-based optimistic locking bool[] includeInWhere = @@ -2636,6 +2644,11 @@ protected internal SqlCommandInfo GenerateUpdateString(bool[] includeProperty, i return hasColumns ? updateBuilder.ToSqlCommandInfo() : null; } + private bool IsPropertyBasedOptimisticLocking(object[] oldFields) + { + return entityMetamodel.OptimisticLockMode > Versioning.OptimisticLock.Version && oldFields != null; + } + private bool CheckVersion(bool[] includeProperty) { return includeProperty[VersionProperty] || entityMetamodel.PropertyUpdateGenerationInclusions[VersionProperty] != ValueInclusion.None; @@ -3219,7 +3232,7 @@ protected bool Update(object id, object[] fields, object[] oldFields, object row if (CheckVersion(includeProperty)) VersionType.NullSafeSet(statement, oldVersion, index, session); } - else if (entityMetamodel.OptimisticLockMode > Versioning.OptimisticLock.Version && oldFields != null) + else if (IsPropertyBasedOptimisticLocking(oldFields)) { bool[] versionability = PropertyVersionability; bool[] includeOldField = OptimisticLockMode == Versioning.OptimisticLock.All @@ -3248,7 +3261,7 @@ protected bool Update(object id, object[] fields, object[] oldFields, object row } else { - return Check(session.Batcher.ExecuteNonQuery(statement), id, j, expectation, statement); + return Check(session.Batcher.ExecuteNonQuery(statement), id, j, expectation, statement, IsPropertyBasedOptimisticLocking(oldFields)); } } catch (StaleStateException e) @@ -3352,7 +3365,7 @@ public void Delete(object id, object version, int j, object obj, SqlCommandInfo { VersionType.NullSafeSet(statement, version, index, session); } - else if (entityMetamodel.OptimisticLockMode > Versioning.OptimisticLock.Version && loadedState != null) + else if (IsPropertyBasedOptimisticLocking(loadedState)) { bool[] versionability = PropertyVersionability; IType[] types = PropertyTypes; @@ -3376,7 +3389,7 @@ public void Delete(object id, object version, int j, object obj, SqlCommandInfo } else { - Check(session.Batcher.ExecuteNonQuery(statement), tableId, j, expectation, statement); + Check(session.Batcher.ExecuteNonQuery(statement), tableId, j, expectation, statement, IsPropertyBasedOptimisticLocking(loadedState)); } } catch (Exception e)