Skip to content

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 6 commits into from
Jan 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion doc/reference/modules/basic_mapping.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2385,6 +2385,7 @@
<area id="onetoone9" coords="10 60"/>
<area id="onetoone10" coords="11 60"/>
<area id="onetoone11" coords="12 60"/>
<area id="onetoone12" coords="13 60"/>
</areaspec>
<programlisting><![CDATA[<one-to-one
name="propertyName"
Expand All @@ -2398,6 +2399,7 @@
formula="any SQL expression"
entity-name="entityName"
foreign-key="foreignKeyName"
optimistic-lock="true|false"
/>]]></programlisting>
<calloutlist>
<callout arearefs="onetoone1">
Expand Down Expand Up @@ -2471,12 +2473,20 @@
<literal>entity-name</literal> (optional): The entity name of the associated class.
</para>
</callout>
<callout arearefs="manytoone11">
<callout arearefs="onetoone11">
<para>
<literal>foreign-key</literal> (optional): Specifies the name of the foreign key
constraint for DDL generation.
</para>
</callout>
<callout arearefs="onetoone12">
<para>
<literal>optimistic-lock</literal> (optional - defaults to <literal>true</literal>):
Specifies that updates to this property do or do not require acquisition of the
optimistic lock. In other words, determines if a version increment should occur when
this property is dirty.
</para>
</callout>
</calloutlist>
</programlistingco>

Expand Down
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));
}
}
}
}
19 changes: 19 additions & 0 deletions src/NHibernate.Test/NHSpecificTest/GH3204/Entity.cs
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; }
}
}
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));
}
}
}
}
6 changes: 6 additions & 0 deletions src/NHibernate/Cfg/MappingSchema/Hbm.generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2922,6 +2922,11 @@ public partial class HbmOneToOne {
[System.ComponentModel.DefaultValueAttribute(false)]
public bool constrained;

/// <remarks/>
[System.Xml.Serialization.XmlAttributeAttribute("optimistic-lock")]
[System.ComponentModel.DefaultValueAttribute(true)]
public bool optimisticlock;

/// <remarks/>
[System.Xml.Serialization.XmlAttributeAttribute("foreign-key")]
public string foreignkey;
Expand All @@ -2944,6 +2949,7 @@ public partial class HbmOneToOne {

public HbmOneToOne() {
this.constrained = false;
this.optimisticlock = true;
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/NHibernate/Cfg/MappingSchema/HbmOneToOne.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ public string Access

public bool OptimisticLock
{
get { return true; }
Copy link
Member Author

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

Copy link
Member

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.

Copy link
Member Author

@bahusoid bahusoid Jan 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks to me the old behavior was a bug

Version update depends in first place on owner object dirtiness. And IsDirty returning false for OneToOne seems was also intentional behavior:

public override bool IsDirty(object old, object current, ISessionImplementor session)
{
return false;

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.

Copy link
Member

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.

Copy link
Member Author

@bahusoid bahusoid Jan 4, 2023

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.

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 to false for one-to-one mapping)

Copy link
Member Author

@bahusoid bahusoid Jan 5, 2023

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?

Copy link
Contributor

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason given was that the IsDirty property should only be used for database updates and not cache related updates

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.

Copy link
Member Author

@bahusoid bahusoid Jan 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likewise I cannot find any utility in incrementing the version number whenever a one-to-one is modified.

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.

Copy link
Contributor

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.

get => optimisticlock;
set => optimisticlock = value;
}

#endregion
Expand Down
2 changes: 1 addition & 1 deletion src/NHibernate/Mapping/ByCode/Impl/OneToOneMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public void Access(System.Type accessorType)

public void OptimisticLock(bool takeInConsiderationForOptimisticLock)
{
// not supported by HbmOneToOne
_oneToOne.optimisticlock = takeInConsiderationForOptimisticLock;
}

#endregion
Expand Down
1 change: 1 addition & 0 deletions src/NHibernate/nhibernate-mapping.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -1040,6 +1040,7 @@
</xs:attribute>
<xs:attribute name="constrained" default="false" type="xs:boolean">
</xs:attribute>
<xs:attribute name="optimistic-lock" default="true" type="xs:boolean" />
<xs:attribute name="foreign-key" type="xs:string" />
<xs:attribute name="property-ref" type="xs:string" />
<xs:attribute name="lazy" type="laziness">
Expand Down