Skip to content
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 support for fetching an individual lazy property with hql and linq provider #1922

Merged
merged 3 commits into from
Jan 30, 2019

Conversation

maca88
Copy link
Contributor

@maca88 maca88 commented Dec 5, 2018

Syntax for fetching in hql

Individual lazy property:

from Person fetch LazyProperty

All lazy properties (already working):

from Person fetch all properties

Syntax for fetching in linq

Individual lazy property:

session.Query<Persion>().Fetch(o => o.LazyProperty).ToList()

All lazy properties:

session.Query<Persion>().FetchLazyProperties().ToList()

@fredericDelaporte fredericDelaporte added this to the 5.3 milestone Dec 5, 2018
@hazzik
Copy link
Member

hazzik commented Dec 6, 2018

I recently looked into the lazy properties feature and so I think this can be implemented simpler. I think it can be started from how AbstractFieldInterceptor implements InitializeField. However, I must admit I did not check the PR's code in the details.

@maca88
Copy link
Contributor Author

maca88 commented Dec 6, 2018

This PR alters the uninitialized field names that are then passed to create a DefaultFieldInterceptor:

if (IsInstrumented && (EntityMetamodel.HasLazyProperties || EntityMetamodel.HasUnwrapProxyForProperties))
{
HashSet<string> lazyProps = lazyPropertiesAreUnfetched && EntityMetamodel.HasLazyProperties ? lazyPropertyNames : null;
if (lazyProps != null && fetchedLazyProperties != null)
{
lazyProps = new HashSet<string>(lazyProps.Except(fetchedLazyProperties));
}
FieldInterceptionHelper.InjectFieldInterceptor(entity, EntityName, this.MappedClass ,lazyProps, unwrapProxyPropertyNames, session);

where in the current master all fields are flagged as uninitialized:

if (IsInstrumented && (EntityMetamodel.HasLazyProperties || EntityMetamodel.HasUnwrapProxyForProperties))
{
HashSet<string> lazyProps = lazyPropertiesAreUnfetched && EntityMetamodel.HasLazyProperties ? lazyPropertyNames : null;
//TODO: if we support multiple fetch groups, we would need
// to clone the set of lazy properties!
FieldInterceptionHelper.InjectFieldInterceptor(entity, EntityName, this.MappedClass ,lazyProps, unwrapProxyPropertyNames, session);

InitializeField is called based on IsUninitializedProperty which checks the fields names that were passed when DefaultFieldInterceptor was created:

private bool IsUninitializedProperty(string fieldName)
{
return uninitializedFields != null && uninitializedFields.Contains(fieldName);
}

There is a lot files changed due to deprecating methods and half of the added files are test cases.

@maca88 maca88 changed the title Add support for fetching an individual lazy property with hql and linq provider WIP - Add support for fetching an individual lazy property with hql and linq provider Dec 8, 2018
@hazzik
Copy link
Member

hazzik commented Dec 10, 2018

Related to #1267.

@maca88
Copy link
Contributor Author

maca88 commented Dec 13, 2018

In the current state, there are still some scenarios that this PR currently does not cover:

  1. update the 2nd level cache after updating an already loaded entity with a new lazy property:
person = s.CreateQuery("from Person fetch Address where Id = 1").UniqueResult<Person>();
person = s.CreateQuery("from Person fetch Formula where Id = 1").UniqueResult<Person>(); // here we should update the 2nd level cache if the entity is cacheable
  1. AbstractFieldInterceptor.InitializeFiled will still initialize all lazy properties even if some of them were already initialized.
person = s.CreateQuery("from Person fetch Address where Id = 1").UniqueResult<Person>();
var value = person.Formula; // will trigger a query for all lazy properties, even Address which was already loaded

For #1267 I've modified the AbstractFieldInterceptor.Intercept method to support the first test (AcceptPropertySetWithTransientObject), for the others we have to modify the FromElement class to store property names which have to be unwrapped, similar to what was done here for lazy properties. Then pass them into IEntityPersister.AfterInitialize so that the injected field interceptor will know which have been fetched (NOTE: the field interceptor is initialized after the SetPropertyValues call, which is the root of the problem). Interesting enough, Hibernate does not send any information about which lazy properties were loaded when injecting the interceptor, they also removed areLazyPropertiesUnfetched from the AfterInitialize method when they added lazy attribute fetch groups. I have still to investigate how this works in Hibernate, which I will next week as I will be away until then.

There is also #1232 that is related to this PR as the logic to detect a component in Linq was done here, for which I have to add some tests to see if it works.

I also noticed that currently if the entity contains a lazy property, all the properties are set with the default setter which uses reflection:

if (!EntityMetamodel.HasLazyProperties && optimizer != null && optimizer.AccessOptimizer != null)
{
SetPropertyValuesWithOptimizer(entity, values);
}
else
{
base.SetPropertyValues(entity, values);

to make them more performant I will probably make a separate PR to resolve that.

@fredericDelaporte
Copy link
Member

About 1., are the two queries done on the same session without evicting? In such case, the second query is supposed to yield the same instance than the first query, discarding its loaded data. So the second lazy-property fetch is likely to even be ignored.
Not updating the second level cache in such case is not much a trouble, since as far as I know NHibernate does not even update the session cache in such case, it discards the loaded data.

About 2., yes, it will load again the already fetched lazy properties when loading an unfetched one, as currently it only knows to lazy-load them all. I think it is acceptable. Maybe, for consistency with 1. behavior, should it just be adapted to ignore the re-loaded values when hydrating lazy-properties.

Lazy attribute fetch group is an interesting feature, but I do not think it will cover the 2. case. It may just mitigate it in some cases. If it is considered to port them, I think it will deserve a dedicated PR.

@maca88
Copy link
Contributor Author

maca88 commented Dec 16, 2018

are the two queries done on the same session without evicting?

Yes, they are.

In such case, the second query is supposed to yield the same instance than the first query, discarding its loaded data

this PR modifies this behavior such that the loaded lazy properties will be updated, so both lazy properties will be lodaded after the two queries (of course we should update it only if it wasn't altered). I think that we should not discard its loading data as it can produce different results with the same query, for example:

using (var s = OpenSession())
{
  // BestFriend property is the same type as Person
  person = s.CreateQuery("from Person p fetch p.Formula left join fetch p.BestFriend bf fetch bf.Address").List<Person>().FirstOrDefault(o => o.Id == 1);
  // in the database there are two persons, one has the BestFriend property set to the second one and the second one does not have it set.
}

the above query can produce different results when running on various databases, depending by the order the rows are retrieved. If the BestFriend is the first instance to be loaded then it will have only the Formula fetched which is not the expected behavior.
If we decide to update the lazy properties after the second query then it makes sense to update also 2nd level cache.

For the second point I think that it deserve a separate PR if we would like to change the current behavior as we would need to change the current SQLLazySelectString property from the AbstractEntityPersister. I do think that the "Lazy attribute fetch group" could solve the problem as the user could set a different group for each lazy property but I can be wrong since I didn't yet analyzed how it works.

@maca88
Copy link
Contributor Author

maca88 commented Dec 19, 2018

With the latest commit the 2nd level cache is now updated if a new lazy property was fetched in a separate query (same session)

person = s.CreateQuery("from Person fetch Address where Id = 1").UniqueResult<Person>();
person = s.CreateQuery("from Person fetch Formula where Id = 1").UniqueResult<Person>(); // here the 2nd level cache is updated

or in the same query that has two occurrences of the same entity with different fetches

person = s.CreateQuery("from Person p fetch p.Formula left join fetch p.BestFriend bf fetch bf.Address order by p.Id desc").List<Person>().FirstOrDefault(o => o.Id == 1);

But does not update it if the lazy property is accessed via the property getter (AbstractFieldInterceptor.InitializeField):

person = s.CreateQuery("from Person fetch Address where Id = 1").UniqueResult<Person>();
var value = person.Formula; // will trigger a query for all lazy properties, even Address which was already loaded

I leaved this for a separate PR that will include the lazy attribute fetch group feature, but I can add it here if desired.

The field interceptor was slightly modified in order to not trigger the lazy properties initialization when one of them is set:

var person = s.Get<Person>(1);
var newValue = "vely looong string";
person.LazyProperty = newValue;

this allows to update the lazy property without having to initialize all of them. This produces a side effect as we don't know if the set value is different from the one that is in the database, so an update will be triggered even if the value is the same. Also the update will ignore the IsDynamicUpdate value if the entity was not initialized, as a custom update must be generated to update only the set lazy property. I think that this change is better than the current one as the user can decide if to update the lazy property without fetching them or not. For having the old behavior a condition should be placed which will initialize all lazy properties:

var newValue = "vely looong string";
if (person.LazyProperty != newValue)
{
    person.LazyProperty = newValue;
}

Also #1232 was solved in latest commit and Linq Fetch method was enhanced to support fetching subclass properties:

s.Query<Animal>().Fetch(o => ((Cat) o).CatLazyProperty).First(o => o.Id == 1);

@fredericDelaporte
Copy link
Member

I leaved this for a separate PR that will include the lazy attribute fetch group feature, but I can add it here if desired.

No, it is better to not widen the scope of PRs.

The field interceptor was slightly modified in order to not trigger the lazy properties initialization when one of them is set

I would rather have this in a separated PR. By the way, does updating to null or default value for the type still work with this change? Since the initial value of the property was not loaded, the dirty check mechanism may overlook such updates.

@maca88
Copy link
Contributor Author

maca88 commented Dec 19, 2018

I would rather have this in a separated PR

Ok, will do.

By the way, does updating to null or default value for the type still work with this change?

Yes it works because LazyPropertyInitializer.UnfetchedProperty is used for unfetched properties.

@maca88 maca88 force-pushed the FetchLazyProperties branch from 04761c6 to 13860d7 Compare January 12, 2019 21:55
@maca88
Copy link
Contributor Author

maca88 commented Jan 12, 2019

Rebased.

@maca88 maca88 changed the title WIP - Add support for fetching an individual lazy property with hql and linq provider Add support for fetching an individual lazy property with hql and linq provider Jan 12, 2019
@fredericDelaporte fredericDelaporte merged commit 116398d into nhibernate:master Jan 30, 2019
@@ -1457,10 +1529,35 @@ public string PropertySelectFragment(string name, string suffix, bool allPropert
int[] columnTableNumbers = SubclassColumnTableNumberClosure;
string[] columnAliases = SubclassColumnAliasClosure;
string[] columns = SubclassColumnClosure;
HashSet<string> fetchColumnsAndFormulas = null;
if (fetchProperties != null)
Copy link
Member

Choose a reason for hiding this comment

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

Why this cannot be simply fetchProperties?.Contains(name)?

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.

3 participants