-
Notifications
You must be signed in to change notification settings - Fork 933
Enable one-to-one optimistic lock handling in mapping #3216
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
83d5bc7
Fix one-to-one optimistic lock handling
bahusoid 3f7eb35
Generate async files
github-actions[bot] 75b716f
Use true as default optimistic-lock mapping option
bahusoid 398b1b2
Merge branch '5.4.x' into gh3204
bahusoid 39abe3f
Use true as default optimistic-lock mapping
bahusoid 5850429
Apply suggestions from code review
bahusoid File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
113 changes: 113 additions & 0 deletions
113
src/NHibernate.Test/Async/NHSpecificTest/GH3204/OneToOneOptimisticLockFixture.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
//------------------------------------------------------------------------------ | ||
// <auto-generated> | ||
// This code was generated by AsyncGenerator. | ||
// | ||
// Changes to this file may cause incorrect behavior and will be lost if | ||
// the code is regenerated. | ||
// </auto-generated> | ||
//------------------------------------------------------------------------------ | ||
|
||
|
||
using System; | ||
using NHibernate.Cfg.MappingSchema; | ||
using NHibernate.Mapping.ByCode; | ||
using NUnit.Framework; | ||
|
||
namespace NHibernate.Test.NHSpecificTest.GH3204 | ||
{ | ||
using System.Threading.Tasks; | ||
[TestFixture(true)] | ||
[TestFixture(false)] | ||
[TestFixture(null)] | ||
public class OneToOneOptimisticLockFixtureAsync : TestCaseMappingByCode | ||
{ | ||
private readonly bool? _optimisticLock; | ||
private Guid _id; | ||
|
||
public OneToOneOptimisticLockFixtureAsync(bool? optimisticLock) | ||
{ | ||
_optimisticLock = optimisticLock; | ||
} | ||
|
||
protected override HbmMapping GetMappings() | ||
{ | ||
var mapper = new ModelMapper(); | ||
mapper.Class<Entity>( | ||
rc => | ||
{ | ||
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); | ||
rc.Property(x => x.Name); | ||
rc.Version(x => x.Version, m => { }); | ||
rc.OneToOne( | ||
x => x.OneToOne, | ||
m => | ||
{ | ||
m.PropertyReference(x => x.Parent); | ||
m.ForeignKey("none"); | ||
m.Cascade(Mapping.ByCode.Cascade.All); | ||
if (_optimisticLock.HasValue) | ||
m.OptimisticLock(_optimisticLock.Value); | ||
}); | ||
}); | ||
|
||
mapper.Class<Child>( | ||
rc => | ||
{ | ||
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); | ||
rc.Property(x => x.Name); | ||
rc.ManyToOne( | ||
x => x.Parent, | ||
m => | ||
{ | ||
m.Unique(true); | ||
m.ForeignKey("none"); | ||
}); | ||
}); | ||
|
||
return mapper.CompileMappingForAllExplicitlyAddedEntities(); | ||
} | ||
|
||
protected override void OnSetUp() | ||
{ | ||
using (var session = OpenSession()) | ||
using (var transaction = session.BeginTransaction()) | ||
{ | ||
var e1 = new Entity { Name = "Bob" }; | ||
session.Save(e1); | ||
transaction.Commit(); | ||
_id = e1.Id; | ||
} | ||
} | ||
|
||
protected override void OnTearDown() | ||
{ | ||
using (var session = OpenSession()) | ||
using (var transaction = session.BeginTransaction()) | ||
{ | ||
session.CreateQuery("delete from System.Object").ExecuteUpdate(); | ||
|
||
transaction.Commit(); | ||
} | ||
} | ||
|
||
[Test] | ||
public async Task GetAsync() | ||
{ | ||
using (var session = OpenSession()) | ||
using (var transaction = session.BeginTransaction()) | ||
{ | ||
var result = await (session.GetAsync<Entity>(_id)); | ||
|
||
var child = new Child() { Parent = result }; | ||
result.OneToOne = child; | ||
var oldVersion = result.Version; | ||
await (transaction.CommitAsync()); | ||
session.Clear(); | ||
|
||
Assert.That( | ||
(await (session.GetAsync<Entity>(_id))).Version, | ||
Is.EqualTo(_optimisticLock.GetValueOrDefault(true) ? oldVersion + 1 : oldVersion)); | ||
} | ||
} | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
using System; | ||
|
||
namespace NHibernate.Test.NHSpecificTest.GH3204 | ||
{ | ||
public class Entity | ||
{ | ||
public virtual Guid Id { get; set; } | ||
public virtual string Name { get; set; } | ||
public virtual int Version { get; set; } | ||
public virtual Child OneToOne { get; set; } | ||
} | ||
|
||
public class Child | ||
{ | ||
public virtual Guid Id { get; set; } | ||
public virtual string Name { get; set; } | ||
public virtual Entity Parent { get; set; } | ||
} | ||
} |
102 changes: 102 additions & 0 deletions
102
src/NHibernate.Test/NHSpecificTest/GH3204/OneToOneOptimisticLockFixture.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
using System; | ||
using NHibernate.Cfg.MappingSchema; | ||
using NHibernate.Mapping.ByCode; | ||
using NUnit.Framework; | ||
|
||
namespace NHibernate.Test.NHSpecificTest.GH3204 | ||
{ | ||
[TestFixture(true)] | ||
[TestFixture(false)] | ||
[TestFixture(null)] | ||
public class OneToOneOptimisticLockFixture : TestCaseMappingByCode | ||
{ | ||
private readonly bool? _optimisticLock; | ||
private Guid _id; | ||
|
||
public OneToOneOptimisticLockFixture(bool? optimisticLock) | ||
{ | ||
_optimisticLock = optimisticLock; | ||
} | ||
|
||
protected override HbmMapping GetMappings() | ||
{ | ||
var mapper = new ModelMapper(); | ||
mapper.Class<Entity>( | ||
rc => | ||
{ | ||
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); | ||
rc.Property(x => x.Name); | ||
rc.Version(x => x.Version, m => { }); | ||
rc.OneToOne( | ||
x => x.OneToOne, | ||
m => | ||
{ | ||
m.PropertyReference(x => x.Parent); | ||
m.ForeignKey("none"); | ||
m.Cascade(Mapping.ByCode.Cascade.All); | ||
if (_optimisticLock.HasValue) | ||
m.OptimisticLock(_optimisticLock.Value); | ||
}); | ||
}); | ||
|
||
mapper.Class<Child>( | ||
rc => | ||
{ | ||
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); | ||
rc.Property(x => x.Name); | ||
rc.ManyToOne( | ||
x => x.Parent, | ||
m => | ||
{ | ||
m.Unique(true); | ||
m.ForeignKey("none"); | ||
}); | ||
}); | ||
|
||
return mapper.CompileMappingForAllExplicitlyAddedEntities(); | ||
} | ||
|
||
protected override void OnSetUp() | ||
{ | ||
using (var session = OpenSession()) | ||
using (var transaction = session.BeginTransaction()) | ||
{ | ||
var e1 = new Entity { Name = "Bob" }; | ||
session.Save(e1); | ||
transaction.Commit(); | ||
_id = e1.Id; | ||
} | ||
} | ||
|
||
protected override void OnTearDown() | ||
{ | ||
using (var session = OpenSession()) | ||
using (var transaction = session.BeginTransaction()) | ||
{ | ||
session.CreateQuery("delete from System.Object").ExecuteUpdate(); | ||
|
||
transaction.Commit(); | ||
} | ||
} | ||
|
||
[Test] | ||
public void Get() | ||
{ | ||
using (var session = OpenSession()) | ||
using (var transaction = session.BeginTransaction()) | ||
{ | ||
var result = session.Get<Entity>(_id); | ||
|
||
var child = new Child() { Parent = result }; | ||
result.OneToOne = child; | ||
var oldVersion = result.Version; | ||
transaction.Commit(); | ||
session.Clear(); | ||
|
||
Assert.That( | ||
session.Get<Entity>(_id).Version, | ||
Is.EqualTo(_optimisticLock.GetValueOrDefault(true) ? oldVersion + 1 : oldVersion)); | ||
} | ||
} | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though it never worked it seems original intention was to always update version. So maybe it should be true by default (as with all other properties). So as an alternative we can document it as possible breaking change with ability to restore old behavior in mapping
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, it looks to me the old behavior was a bug. As such we should not restore it, and it is not even a possible breaking change, but an undocumented bug fix.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Version update depends in first place on owner object dirtiness. And
IsDirty
returning false forOneToOne
seems was also intentional behavior:nhibernate-core/src/NHibernate/Type/OneToOneType.cs
Lines 60 to 62 in 37b5020
So it's hard to say where the problem is considering that intentional behavior is not documented and not changeable and old behavior was always there.
So my thought on this - yes 5.4.0 default behavior looks legit (behaves consistently with other mappings). But then we should allow to restore back to old behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought it could still be changed through mappings. If that is the case, it seems enough to me. Introducing some setting to restore a previous behavior akin to a bug (by principle of consistency at least) looks overkill to me.
I understand it means affected applications will have to fix their mapping, which can be huge. But when code relies on a behavior undocumented and inconsistent with similar features, I think that is acceptable to offer no better than instructions for fixing the code when said behavior is fixed.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope. This PR implements ability to control it through mappings.
Ok so to conclude: we keep new behavior as it's consistent with other mappings. But this PR allows to restore old behavior in mapping (by setting
optimistic-lock
tofalse
forone-to-one
mapping)Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm. #2576 was also ported to hibernate so I checked what's going on there. They reverted this change completely hibernate/hibernate-orm#5330
And the reason seems to be similar to this issue @deAtog Can you shed light on it? Should we be worried too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my opinion, the OptimisticLock property should always return false. Prior to the changes I made, the IsDirty property of the one-to-one always returned false. As such the version property was never incremented if the value of the one-to-one was modified. The changes I made to the IsDirty property caused a regression with versioning. Whenever a transient child was associated with a persistent parent it caused the version of the parent to be incremented. None of my original tests included versioning, so this issue was not discovered at that time.
Due the regression, the nhibernate-orm developers decided to revert my changes to address the caching issue. The reason given was that the IsDirty property should only be used for database updates and not cache related updates. Since a one-to-one does not have a backing field in the database, they felt it should never be dirty. Needless to say, there is no way to currently differentiate between a cache or database related update as the two are currently combined into a single update event based upon the IsDirty property. As such, I disagree with their analysis that the IsDirty property should be reserved solely for database related changes. Likewise I cannot find any utility in incrementing the version number whenever a one-to-one is modified. As such it is my opinion that the OptimisticLock property should always return false for a one-to-one as stated above.
For example, modifying a constrained one-to-one property of a child with a foreign key identifier currently does not change the identifier of the child. Likewise, adding or removing a child from a persistent parent does not modify the parent. As such, there is little utility in optimistic locking in either of these cases as they would only increment the version number.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't quite understand what "database update" means. Does it mean some columns from owner table need to be modified? But it doesn't work this way for collection mappings. If collection is modified owner is considered dirty (though no owner columns are modified by this update) In this regard I don't see much difference between collection mapping and one-to-one mapping.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same reasons as with any other version updates - to avoid concurrent owner modifications even indirect (like with collections) So I'm not convinced I still think current 5.4 behavior is consistent with other mappings and with this PR can be configured to restore old behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A "database update" refers to a modification to existing data in the database. E.g. when you add a child to a persistent parent, the child creates an insert into a related table but ordinarily does not modify the existing data of the parent. Thus the one-to-one on the Parent though dirty does not normally cause a database update. As I stated above I disagree with their assessment of the IsDirty property.
I'm okay with these changes as the default for OptimisticLock is set to false. Personally, I would never set it to true, but I guess you have an argument for where it might be useful despite the inherent performance implications.