-
Notifications
You must be signed in to change notification settings - Fork 934
Fix deletion of component with null property #1755
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
Changes from all commits
d8a7c86
81dc1d2
f9455c0
dc03a72
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
//------------------------------------------------------------------------------ | ||
// <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.Linq; | ||
using NUnit.Framework; | ||
using NHibernate.Linq; | ||
|
||
namespace NHibernate.Test.NHSpecificTest.GH1170 | ||
{ | ||
using System.Threading.Tasks; | ||
[TestFixture] | ||
public class FixtureAsync : BugTestCase | ||
{ | ||
// Only the set case is tested, because other cases were not affected: | ||
// - bags delete everything first. | ||
// - indexed collections use their index, which is currently not mappable as a composite index with nullable | ||
// column. All index columns are forced to not-nullable by mapping implementation. When using a formula in | ||
// index, they use the element, but its columns are also forced to not-nullable. | ||
|
||
[Test] | ||
public async Task DeleteComponentWithNullAsync() | ||
{ | ||
using (var session = OpenSession()) | ||
using (var tx = session.BeginTransaction()) | ||
{ | ||
var parent = await (session.Query<Parent>().SingleAsync()); | ||
Assert.That( | ||
parent.ChildComponents, | ||
Has.Count.EqualTo(2).And.One.Property(nameof(ChildComponent.SomeString)).Null); | ||
parent.ChildComponents.Remove(parent.ChildComponents.Single(c => c.SomeString == null)); | ||
await (tx.CommitAsync()); | ||
} | ||
|
||
using (var session = OpenSession()) | ||
using (var tx = session.BeginTransaction()) | ||
{ | ||
var parent = await (session.Query<Parent>().SingleAsync()); | ||
Assert.That( | ||
parent.ChildComponents, | ||
Has.Count.EqualTo(1).And.None.Property(nameof(ChildComponent.SomeString)).Null); | ||
await (tx.CommitAsync()); | ||
} | ||
} | ||
|
||
[Test] | ||
public async Task UpdateComponentWithNullAsync() | ||
{ | ||
// Updates on set are indeed handled as delete/insert, so this test is not really needed. | ||
using (var session = OpenSession()) | ||
using (var tx = session.BeginTransaction()) | ||
{ | ||
var parent = await (session.Query<Parent>().SingleAsync()); | ||
Assert.That( | ||
parent.ChildComponents, | ||
Has.Count.EqualTo(2).And.One.Property(nameof(ChildComponent.SomeString)).Null); | ||
parent.ChildComponents.Single(c => c.SomeString == null).SomeString = "no more null"; | ||
await (tx.CommitAsync()); | ||
} | ||
|
||
using (var session = OpenSession()) | ||
using (var tx = session.BeginTransaction()) | ||
{ | ||
var parent = await (session.Query<Parent>().SingleAsync()); | ||
Assert.That( | ||
parent.ChildComponents, | ||
Has.Count.EqualTo(2).And.None.Property(nameof(ChildComponent.SomeString)).Null); | ||
await (tx.CommitAsync()); | ||
} | ||
} | ||
|
||
protected override void OnSetUp() | ||
{ | ||
using (var session = OpenSession()) | ||
using (var tx = session.BeginTransaction()) | ||
{ | ||
var parent = new Parent(); | ||
parent.ChildComponents.Add(new ChildComponent { SomeBool = true, SomeString = "something" }); | ||
parent.ChildComponents.Add(new ChildComponent { SomeBool = false, SomeString = null }); | ||
session.Save(parent); | ||
|
||
tx.Commit(); | ||
} | ||
} | ||
|
||
protected override void OnTearDown() | ||
{ | ||
using (var session = OpenSession()) | ||
using (var tx = session.BeginTransaction()) | ||
{ | ||
session.Delete("from Parent"); | ||
tx.Commit(); | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
using System.Linq; | ||
using NUnit.Framework; | ||
|
||
namespace NHibernate.Test.NHSpecificTest.GH1170 | ||
{ | ||
[TestFixture] | ||
public class Fixture : BugTestCase | ||
{ | ||
// Only the set case is tested, because other cases were not affected: | ||
// - bags delete everything first. | ||
// - indexed collections use their index, which is currently not mappable as a composite index with nullable | ||
// column. All index columns are forced to not-nullable by mapping implementation. When using a formula in | ||
// index, they use the element, but its columns are also forced to not-nullable. | ||
|
||
[Test] | ||
public void DeleteComponentWithNull() | ||
{ | ||
using (var session = OpenSession()) | ||
using (var tx = session.BeginTransaction()) | ||
{ | ||
var parent = session.Query<Parent>().Single(); | ||
Assert.That( | ||
parent.ChildComponents, | ||
Has.Count.EqualTo(2).And.One.Property(nameof(ChildComponent.SomeString)).Null); | ||
parent.ChildComponents.Remove(parent.ChildComponents.Single(c => c.SomeString == null)); | ||
tx.Commit(); | ||
} | ||
|
||
using (var session = OpenSession()) | ||
using (var tx = session.BeginTransaction()) | ||
{ | ||
var parent = session.Query<Parent>().Single(); | ||
Assert.That( | ||
parent.ChildComponents, | ||
Has.Count.EqualTo(1).And.None.Property(nameof(ChildComponent.SomeString)).Null); | ||
tx.Commit(); | ||
} | ||
} | ||
|
||
[Test] | ||
public void UpdateComponentWithNull() | ||
{ | ||
// Updates on set are indeed handled as delete/insert, so this test is not really needed. | ||
using (var session = OpenSession()) | ||
using (var tx = session.BeginTransaction()) | ||
{ | ||
var parent = session.Query<Parent>().Single(); | ||
Assert.That( | ||
parent.ChildComponents, | ||
Has.Count.EqualTo(2).And.One.Property(nameof(ChildComponent.SomeString)).Null); | ||
parent.ChildComponents.Single(c => c.SomeString == null).SomeString = "no more null"; | ||
tx.Commit(); | ||
} | ||
|
||
using (var session = OpenSession()) | ||
using (var tx = session.BeginTransaction()) | ||
{ | ||
var parent = session.Query<Parent>().Single(); | ||
Assert.That( | ||
parent.ChildComponents, | ||
Has.Count.EqualTo(2).And.None.Property(nameof(ChildComponent.SomeString)).Null); | ||
tx.Commit(); | ||
} | ||
} | ||
|
||
protected override void OnSetUp() | ||
{ | ||
using (var session = OpenSession()) | ||
using (var tx = session.BeginTransaction()) | ||
{ | ||
var parent = new Parent(); | ||
parent.ChildComponents.Add(new ChildComponent { SomeBool = true, SomeString = "something" }); | ||
parent.ChildComponents.Add(new ChildComponent { SomeBool = false, SomeString = null }); | ||
session.Save(parent); | ||
|
||
tx.Commit(); | ||
} | ||
} | ||
|
||
protected override void OnTearDown() | ||
{ | ||
using (var session = OpenSession()) | ||
using (var tx = session.BeginTransaction()) | ||
{ | ||
session.Delete("from Parent"); | ||
tx.Commit(); | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?xml version="1.0" encoding="utf-8" ?> | ||
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" | ||
assembly="NHibernate.Test" | ||
namespace="NHibernate.Test.NHSpecificTest.GH1170"> | ||
<class name="Parent"> | ||
<id name="Id" column="id_column_of_parent"> | ||
<generator class="guid" /> | ||
</id> | ||
<set name="ChildComponents"> | ||
<key column="key_column_of_child" /> | ||
<composite-element class="ChildComponent"> | ||
<property name="SomeBool" /> | ||
<property name="SomeString" /> | ||
</composite-element> | ||
</set> | ||
</class> | ||
</hibernate-mapping> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
|
||
namespace NHibernate.Test.NHSpecificTest.GH1170 | ||
{ | ||
public class Parent | ||
{ | ||
public virtual ICollection<ChildComponent> ChildComponents { get; set; } = new List<ChildComponent>(); | ||
public virtual Guid Id { get; set; } | ||
} | ||
|
||
public class ChildComponent | ||
{ | ||
public virtual bool SomeBool { get; set; } | ||
public virtual string SomeString { get; set; } | ||
|
||
protected bool Equals(ChildComponent other) | ||
{ | ||
return SomeBool == other.SomeBool && string.Equals(SomeString, other.SomeString); | ||
} | ||
|
||
public override bool Equals(object obj) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not overriding it causes the set to always delete all its elements, causing the update test to be even more moot. (It is anyway somewhat moot, as with set, updates are handled by delete/insert.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this behaviour needs to be changed (in a separate PR). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean, if the component reference has not changed, update it instead of deleting/inserting it? But the persistence context loaded state does not keep track of component references and so, does not allow to check for that condition. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm talking about this behaviour: "Not overriding it causes the set to always delete all its elements" it shouldn't be like this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From here:
It is in fact an error to map a set of components without a proper equal semantic. The set cannot enforce its semantic in such case: better use a bag instead. Of course the side effect this error has about flushes, causing the whole set to be purged and recreated, could still be fixed. But that would be fixing a case resulting anyway from a bad mapping/implementation on the user side. Why not if this can be done in a simple way with very few impacts on normal uses, but otherwise, I do not think it should be fixed. Fixing it will likely require to extract the set content as component values arrays, then searching for each array of values if we can find the same array in the loaded state. This looks to me as a non-negligible overhead, compared to just using the set |
||
{ | ||
if (ReferenceEquals(null, obj)) return false; | ||
if (ReferenceEquals(this, obj)) return true; | ||
return obj is ChildComponent other && Equals(other); | ||
} | ||
|
||
public override int GetHashCode() | ||
{ | ||
unchecked | ||
{ | ||
return (SomeBool.GetHashCode() * 397) ^ (SomeString != null ? SomeString.GetHashCode() : 0); | ||
} | ||
} | ||
} | ||
} |
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.
This "forced to not-nullable" thing seems to me a bit undue, as this impacts only ddl generation, forbidding to test such cases (unless tweaking the generated schema afterward), but not forbidding users not using schema generation to map such things, and insert null. Then these cases will have the delete issue too, and also an issue on update. But no one seems to ask for nullable support on those cases, so I have not handled them.