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)