-
Notifications
You must be signed in to change notification settings - Fork 930
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
Add new collection operation queue mechanism #2010
Add new collection operation queue mechanism #2010
Conversation
// (It is also likely one-to-many lists have a similar issue, but nothing is done yet for them. And | ||
// their case is more complex due to having to care for the indexes and to handle many more delayed | ||
// operation kinds.) | ||
if (CollectionPersister.IsOneToMany && loadedCollection.Any(l => ReferenceEquals(l, toAdd))) |
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.
Copied from PersistentGenericBag<>.SimpleAddDelayedOperation
. Instead of the proposed fixes I will try to solve the issue by removing the queued operation for the entity that is immediately inserted because of the id generator from all uninitialized collections that currently contain it. I will try to make a separated PR on top of this one if the mentioned solution will work.
From reading this PR description, it seems to me that this PR will cause more operations on extra-lazy collections to cause some database queries, for the sake of better honouring the collection contract. Returning the correct index when adding to an uninitialized list is surely done by initializing the list count of elements by querying this count in the database. (I guess the next adds, or checks for insert/remove at/set at index range, will not re-trigger a count in db.) Returning The only documentation for extra lazy seems currently to be:
(From here.) So, nothing contradicting what this PR does. But it seemed to me there was a choice made for extra lazy to somewhat break collection contracts when it allows avoiding a database query. |
Correct.
Again correct, the count query is triggered only once when the requested operation needs it and it is stored for further operations.
That is partially correct,
I've updated the set breaking change as it was wrongly written. The query for checking an element existance is already done by the current code: nhibernate-core/src/NHibernate/Collection/Generic/PersistentGenericSet.cs Lines 308 to 325 in 6cb478f
The problem with the current code is when a new entity is added twice in the set, both times
I didn't test this scenario yet, but the same issue applies for the current code as there is no existence check when adding an item to an indexed collection.
As already mentioned this check is already in the current code and that is why I optimized |
This potentially could be a pretty serious drawback. As if I understand correctly there is now no way to "lazy add" via |
It is possible only when the collection is mapped as |
With the latest commit the set optimization for |
I've added some tests that show how this PRs works when an element is added multiple times. The behavior is indeed different that the current one for lists as currently, when checking the |
gavin.Companies.Add(addedItems[i]); | ||
} | ||
|
||
Assert.That(gavin.Companies.Count, Is.EqualTo(10)); |
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.
By running the test with the current code, Count
would be equal to 5
.
[TestCase(false, true)] | ||
[TestCase(true, false)] | ||
[TestCase(true, true)] | ||
public async Task ListAddDuplicatedAsync(bool initialize, bool flush, CancellationToken cancellationToken = default(CancellationToken)) |
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.
@maca88 can you please update AsyncGenerator's NUnit plugin not to generate cancellation token for [TestCase]
?
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.
Done. Also updated the NHibernate configuration #2126.
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 just realised that this is because the test method lacks [Theory]
/[Test]
attribute.
@fredericDelaporte @bahusoid any conclusions on this PR? |
As I've already said I don't like both changes. IMHO it limits functionality (kills delayed add feature for lazy |
@bahusoid I will revert those changes and update the tests. |
Reverted |
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.
Here are some preliminary comments on the test. I will go on later.
src/NHibernate/Collection/Trackers/AbstractCollectionQueueOperationTracker.cs
Outdated
Show resolved
Hide resolved
src/NHibernate/Collection/Trackers/AbstractQueueOperationTracker.cs
Outdated
Show resolved
Hide resolved
@bahusoid, you were having some concerns about some changes in this PR, which have been reverted. Do you have some other concerns? |
Nope. I can't make proper code review but in principle it's all good changes |
We have forgotten to label this as "possible breaking change" and so it has not be mentioned in the possible breaking changes section of the release notes. |
I've added the missing possible breaking change that is related to #2476. |
I am not seeing where you have added that. I am not seeing a change on the release notes. (I usually include missing possible breaking changes in the release pr of the next patch, with a note in that release notes about completing the possible breaking changes of the minor.) |
I've added the missing one in the description of this PR and now I've made a PR for them. Feel free to reword the added notes, as I am not that good at writing :) |
The main purpose of this PR is to minimize flushes when several operations are done to an uninitialized collection. Currently, a flush will occur after a second operation as already explained by @fredericDelaporte, which can lead to various unexpected behavior like throwing a
PropertyValueException
with a messagenot-null property references a null or transient value
, when adding an item to the collection or checking its existence, which occurred to me several times in the past.The new mechanism in this PR replaces the current one which eliminates the need for a flush for all collections except for two cases when using
PersistentGenericList<>
:IList<>.RemoveAt
after usingIList<>.Add
)IList<>.Remove
after using an indexed operation (e.g. useIList<>.Remove
after usingIList<>.Insert
)We could eliminate the flushing also for the above cases if we would add an additional method to
ICollectionPersister
, which would return the index of the given element that could be also used to queueIList<>.IndexOf
operation. This can be done in a separate PR if desired.In addition an optimization was done for
PersistentGenericSet<>
when usingICollection<>.Add
orISet<>.Add
for a transient entity (only when we are able to detect that it is transient) which will not trigger a query for checking its existence.This PR contains several breaking changes (applies for collections mapped with
inverse="true"
):CallingIList.Add
for an uninitialized collection mapped aslazy="true"
will trigger the collection initialization and return the correct index, currently the collection is not initialized and-1
is returned (applies for Bag and List)CallingIList.Add
for an uninitialized collection mapped aslazy="extra"
will return the correct index, currently-1
is returned (applies for Bag and List)IList.RemoveAt
orIList<>.RemoveAt
with a negative number will throw anArgumentOutOfRangeException
in order to mimicList<>.RemoveAt
behavior, currently it throws aIndexOutOfRangeException
(applies for List)IList.RemoveAt
orIList<>.RemoveAt
with a number that is equal or higher that the current collection size for an uninitialized collection mapped aslazy="extra"
will throw anArgumentOutOfRangeException
, currently it allows to queue the operation without throwing an exception (applies for List)IList.Insert
orIList<>.Insert
with a negative number will throw anArgumentOutOfRangeException
in order to mimicList<>.Insert
behavior, currently it throws anIndexOutOfRangeException
(applies for List)IList.Insert
orIList<>.Insert
with a number that is higher that the current collection size for an uninitialized collection mapped aslazy="extra"
will throw anArgumentOutOfRangeException
, currently it allows to queue the operation without throwing an exception (applies for List)IList.this[int index]
orIList<>.this[int index]
with a negative number will throw anArgumentOutOfRangeException
in order to mimicList<>.this[int index]
behavior, currently it throws anIndexOutOfRangeException
(applies for List)IList.this[int index]
orIList<>.this[int index]
with a number that is equal or higher that the current collection size for an uninitialized collection mapped aslazy="extra"
will throw anArgumentOutOfRangeException
, currently it allows to queue the operation without throwing an exception (applies for List)IDictionary<,>.Add
orICollection<>.Add
for an uninitialized collection mapped aslazy="extra"
with a key that already exists will throw anArgumentException
, currently it allows to queue the operation without throwing an exception (applies for Map)IDictionary<,>.Remove
orICollection<>.Remove
for an uninitialized collection mapped aslazy="extra"
with a key that does not exist will return false, currently it returns true (applies for Map)EqualityComparer<TValue>.Default
when setting an existing key value withIDictionary<,>.this[]
for an initialized collection, currently the old and the new value are compared by usingReferenceEquals
(applies for Map)ISet<>.Add
for an uninitialized collection mapped aslazy="extra"
with a transient element that already exists in the set will return false, currently it returns true (applies for Set)ISet<>.Add
for an uninitialized collection mapped aslazy="true"
with a transient element that does not overrideEquals
method will not initialize the collection.