Skip to content

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

Merged
merged 4 commits into from
Jul 4, 2018

Conversation

fredericDelaporte
Copy link
Member

@fredericDelaporte fredericDelaporte commented Jun 17, 2018

Fixes #1170 - Incorrect query when items removed from a collection
of components contain null values

NH-3634 has added support of null valued component properties in select queries comparisons, but they are indeed still not properly supported since this still fails when deleting.

hazzik
hazzik previously approved these changes Jun 18, 2018
@fredericDelaporte

This comment has been minimized.

@hazzik hazzik changed the title Add test case for deletion of component with null property [WIP] - Add test case for deletion of component with null property Jun 18, 2018
@fredericDelaporte fredericDelaporte changed the title [WIP] - Add test case for deletion of component with null property Add test case for deletion of component with null property Jun 25, 2018
@fredericDelaporte fredericDelaporte changed the title Add test case for deletion of component with null property Fix deletion of component with null property Jun 25, 2018
@fredericDelaporte
Copy link
Member Author

All commits should be squashed.

The last commit may be dropped if preferred: it could be a possible breaking change for external users of the impacted utility methods, if they use it on array of a type implementing IEquatable without overriding object.Equals or with an object.Equals override having a different semantic than the one from IEquatable. The previous implementation was always delegating equality to object.Equals.

// - 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.
Copy link
Member Author

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.

return SomeBool == other.SomeBool && string.Equals(SomeString, other.SomeString);
}

public override bool Equals(object obj)
Copy link
Member Author

@fredericDelaporte fredericDelaporte Jun 25, 2018

Choose a reason for hiding this comment

The 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.)

Copy link
Member

Choose a reason for hiding this comment

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

I think this behaviour needs to be changed (in a separate PR).

Copy link
Member Author

Choose a reason for hiding this comment

The 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.
Or do you mean we should somewhat "recycle" components to delete by updating them with data of components to add in order to replace as most as possible deletes/inserts by single updates? That would cause actual removals combined with adds to be replaced by updates, which could be quite unexpected for users, and breaking for custom auditing logic (like trigger based auditing).

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

@fredericDelaporte fredericDelaporte Jul 4, 2018

Choose a reason for hiding this comment

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

From here:

Note: if you define an ISet of composite elements, it is very important to implement Equals() and GetHashCode() correctly.

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 Contains method. Moreover it would then ignore equals implementations having another semantic, which will be a possible breaking change.

@@ -468,7 +471,10 @@ public AbstractCollectionPersister(Mapping.Collection collection, ICacheConcurre
?? ExecuteUpdateResultCheckStyle.DetermineDefault(collection.CustomSQLUpdate, updateCallable);
}

// 6.0 TODO: call GenerateDeleteRowString(null); instead.
#pragma warning disable 618
sqlDeleteRowString = GenerateDeleteRowString();
Copy link
Member Author

Choose a reason for hiding this comment

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

Not replaced yet for avoiding a possible breaking change for those having overridden it.

Demonstrate nhibernate#1170 - Incorrect query when items removed from a collection
of components contain null values
All its current usages are done on type where using the default comparer
will not yield different results, excepted in some cases better
performances.
@fredericDelaporte
Copy link
Member Author

Rebased for also updating the documentation. (Fixup commit already squashed.)

@@ -179,45 +179,12 @@ public static int CountTrue(bool[] array)

public static bool ArrayEquals<T>(T[] a, T[] b)
Copy link
Member

@hazzik hazzik Jun 29, 2018

Choose a reason for hiding this comment

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

Turns out this magic already exists in the .NET Fx:

Array implements IStructuralEquatable interface:

var a = new[] {1};
var b = new[] {1};
IStructuralEquatable aa = a;

var @equals = aa.Equals(b, EqualityComparer<int>.Default); //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.

But that magic works by comparing everything as objects as it uses the non generic IEqualityComparer interface, causing value types to be boxed.
The ArrayEquals(byte[] a, byte[] b) overload would be likely less efficient if re-implemented with this.
The way I have re-implemented this (with the last commit) also allows ArrayEquals<T> to avoid boxing.

@fredericDelaporte fredericDelaporte merged commit 3adcfa7 into nhibernate:master Jul 4, 2018
@fredericDelaporte fredericDelaporte deleted the GH1170 branch July 4, 2018 14:16
@israelaece
Copy link

I'm using the NH 5.3.5 and I continue dealing with this issue (removing itens from <set> collection), but, in my case, the composite-element has three properties, two of them are many-to-one.

Note that my DocumentPrefix property is not-null="true". When I set this property to not-null="false" the problem goes away, but in terms of my business domain, this property is not null. Anyway, I can live with this behavior; I'm just reporting the issue to listen your opinion if this is by design or not. Thanks a lot!

<set name="Itens"
     cascade="all-delete-orphan"
     lazy="true"
     access="field.camelcase">
  <key column="ParentId"/>
  <composite-element class="Avalista">
    <property name="DocumentPrefix"
              type="AnsiString"
              not-null="true">
      <column name="DocumentPrefix"
              not-null="true"
              sql-type="varchar(11)" />
    </property>
    <many-to-one name="Person"
                 column="PersonId"
                 not-null="false"
                 lazy="false"
                 class="Person" />
    <many-to-one name="Company"
                 column="CompanyId"
                 not-null="false"
                 lazy="false"
                 class="Company" />
  </composite-element>
</set>

@fredericDelaporte
Copy link
Member Author

It is better open a new issue for this, with a more complete example. (With which data does it fails, ...)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

NH-3646 - Incorrect query when items removed from a collection of components contain null values
3 participants