Skip to content

Port Hibernate's lazy attribute fetch groups #1949

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
Feb 14, 2019
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
44 changes: 32 additions & 12 deletions doc/reference/modules/basic_mapping.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1694,6 +1694,7 @@
<area id="property15" coords="16 55" />
<area id="property16" coords="17 55" />
<area id="property17" coords="18 55" />
<area id="property18" coords="19 55" />
</areaspec>
<programlisting><![CDATA[<property
name="propertyName"
Expand All @@ -1706,6 +1707,7 @@
optimistic-lock="true|false"
generated="never|insert|always"
lazy="true|false"
lazy-group="groupName"
not-null="true|false"
unique="true|false"
unique-key="uniqueKeyName"
Expand Down Expand Up @@ -1773,55 +1775,63 @@
<literal>lazy</literal> (optional - defaults to <literal>false</literal>):
specifies that this property is lazy. A lazy property is not loaded when
the object is initially loaded, unless the fetch mode has been overridden
in a specific query. Values for lazy properties are loaded when any lazy
property of the object is accessed. Having lazy properties causes instances
of the entity to be loaded as proxies. Theses proxies ignore the class
<literal>proxy</literal> setting and always derives from the persistent class,
requiring its members to be overridable.
in a specific query. Values for lazy properties are loaded per lazy-group.
</para>
<para>
Having lazy properties causes instances of the entity to be loaded as proxies.
Theses proxies ignore the class <literal>proxy</literal> setting and always
derives from the persistent class, requiring its members to be overridable.
</para>
</callout>
<callout arearefs="property11">
<para>
<literal>lazy-group</literal> (optional - defaults to <literal>DEFAULT</literal>):
if the property is lazy, its lazy-loading group. When a lazy property is accessed,
the other lazy properties of the lazy group are also loaded with it.
</para>
</callout>
<callout arearefs="property11">
<callout arearefs="property12">
<para>
<literal>not-null</literal> (optional - defaults to <literal>false</literal>):
sets the column nullability for DDL generation.
</para>
</callout>
<callout arearefs="property12">
<callout arearefs="property13">
<para>
<literal>unique</literal> (optional - defaults to <literal>false</literal>):
sets the column uniqueness for DDL generation. Use <literal>unique-key</literal>
instead if the value is unique only in combination with other properties.
</para>
</callout>
<callout arearefs="property13">
<callout arearefs="property14">
<para>
<literal>unique-key</literal> (optional):
a logical name for an unique index for DDL generation. The column will be included in
the index, along with other columns sharing the same <literal>unique-key</literal>
logical name. The actual index name depends on the dialect.
</para>
</callout>
<callout arearefs="property14">
<callout arearefs="property15">
<para>
<literal>index</literal> (optional):
a logical name for an index for DDL generation. The column will be included in
the index, along with other columns sharing the same <literal>index</literal> logical
name. The actual index name depends on the dialect.
</para>
</callout>
<callout arearefs="property15">
<callout arearefs="property16">
<para>
<literal>length</literal> (optional): if the type takes a length and does not
already specify it, its length.
</para>
</callout>
<callout arearefs="property16">
<callout arearefs="property17">
<para>
<literal>precision</literal> (optional): if the type takes a precision and does not
already specify it, its precision.
</para>
</callout>
<callout arearefs="property17">
<callout arearefs="property18">
<para>
<literal>scale</literal> (optional): if the type takes a scale and does not
already specify it, its scale.
Expand Down Expand Up @@ -2598,6 +2608,7 @@
<area id="component6" coords="7 60"/>
<area id="component7" coords="8 60"/>
<area id="component8" coords="9 60"/>
<area id="component9" coords="10 60"/>
</areaspec>
<programlisting><![CDATA[<component
name="propertyName"
Expand All @@ -2607,6 +2618,7 @@
access="field|property|nosetter|className"
optimistic-lock="true|false"
lazy="true|false"
lazy-group="groupName"
unique="true|false">

<property ... />
Expand Down Expand Up @@ -2660,6 +2672,14 @@
</para>
</callout>
<callout arearefs="component8">
<para>
<literal>lazy-group</literal> (optional - defaults to <literal>DEFAULT</literal>):
If the component is lazy, its lazy-loading group. When a lazy property is accessed
on an object, included when the property is a component, the other lazy properties
of the lazy group are also loaded with it.
</para>
</callout>
<callout arearefs="component9">
<para>
<literal>unique</literal> (optional - defaults to <literal>false</literal>): Specifies
that an unique constraint exists upon all mapped columns of the component.
Expand Down
5 changes: 2 additions & 3 deletions src/NHibernate.DomainModel/Async/CustomPersister.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,8 @@ public async Task<object> LoadAsync(object id, object optionalObject, LockMode l
if (obj != null)
{
clone = (Custom)obj.Clone();
TwoPhaseLoad.AddUninitializedEntity(session.GenerateEntityKey(id, this), clone, this, LockMode.None, false,
session);
TwoPhaseLoad.PostHydrate(this, id, new String[] {obj.Name}, null, clone, LockMode.None, false, session);
TwoPhaseLoad.AddUninitializedEntity(session.GenerateEntityKey(id, this), clone, this, LockMode.None, session);
TwoPhaseLoad.PostHydrate(this, id, new String[] {obj.Name}, null, clone, LockMode.None, session);
await (TwoPhaseLoad.InitializeEntityAsync(clone, false, session, new PreLoadEvent((IEventSource) session),
new PostLoadEvent((IEventSource) session), cancellationToken));
}
Expand Down
5 changes: 2 additions & 3 deletions src/NHibernate.DomainModel/CustomPersister.cs
Original file line number Diff line number Diff line change
Expand Up @@ -310,9 +310,8 @@ public object Load(object id, object optionalObject, LockMode lockMode, ISession
if (obj != null)
{
clone = (Custom)obj.Clone();
TwoPhaseLoad.AddUninitializedEntity(session.GenerateEntityKey(id, this), clone, this, LockMode.None, false,
session);
TwoPhaseLoad.PostHydrate(this, id, new String[] {obj.Name}, null, clone, LockMode.None, false, session);
TwoPhaseLoad.AddUninitializedEntity(session.GenerateEntityKey(id, this), clone, this, LockMode.None, session);
TwoPhaseLoad.PostHydrate(this, id, new String[] {obj.Name}, null, clone, LockMode.None, session);
TwoPhaseLoad.InitializeEntity(clone, false, session, new PreLoadEvent((IEventSource) session),
new PostLoadEvent((IEventSource) session));
}
Expand Down
4 changes: 1 addition & 3 deletions src/NHibernate.Test/Async/CacheTest/SerializationFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,7 @@ private CacheEntry CreateCacheEntry()
{
DisassembledState = GetAllKnownTypeValues(),
Version = 55,
Subclass = "Test",
AreLazyPropertiesUnfetched = true
Subclass = "Test"
};
}

Expand Down Expand Up @@ -240,7 +239,6 @@ private void CheckCacheEntry(CacheEntry original, CacheEntry copy)
Assert.That(copy.Version, Is.EqualTo(original.Version));
Assert.That(copy.Version, Is.TypeOf(original.Version.GetType()));
Assert.That(copy.Subclass, Is.EqualTo(original.Subclass));
Assert.That(copy.AreLazyPropertiesUnfetched, Is.EqualTo(original.AreLazyPropertiesUnfetched));
for (var i = 0; i < copy.DisassembledState.Length; i++)
{
Assert.That(copy.DisassembledState[i], Is.TypeOf(original.DisassembledState[i].GetType()));
Expand Down
230 changes: 230 additions & 0 deletions src/NHibernate.Test/Async/LazyGroup/LazyGroupFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
//------------------------------------------------------------------------------
// <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.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NHibernate.Cache;
using NHibernate.Cfg;
using NUnit.Framework;

namespace NHibernate.Test.LazyGroup
{
using System.Threading;
[TestFixture]
public class LazyGroupFixtureAsync : TestCase
{
protected override string MappingsAssembly => "NHibernate.Test";

protected override string[] Mappings => new[] { "LazyGroup.Mappings.hbm.xml" };

protected override void Configure(Configuration configuration)
{
base.Configure(configuration);
configuration.Properties[Environment.CacheProvider] = typeof(HashtableCacheProvider).AssemblyQualifiedName;
configuration.Properties[Environment.UseSecondLevelCache] = "true";
configuration.Properties[Environment.GenerateStatistics] = "true";
}

protected override void OnSetUp()
{
using (var s = OpenSession())
using (var tx = s.BeginTransaction())
{
for (var i = 1; i <= 5; i++)
{
var person = GeneratePerson(i);
s.Save(person);
}

tx.Commit();
}
}

protected override void OnTearDown()
{
using (var s = OpenSession())
using (var tx = s.BeginTransaction())
{
s.CreateQuery("delete from Person").ExecuteUpdate();
tx.Commit();
}
}

[Test]
public async Task TestGroupsAsync()
{
using (var s = OpenSession())
{
var person = await (s.GetAsync<Person>(1));
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Name"), Is.True);
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "NickName"), Is.False);
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False);
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False);
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Age"), Is.False);

var nickName = person.NickName;
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "NickName"), Is.True);
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False);
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False);
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Age"), Is.False);
Assert.That(nickName, Is.EqualTo("NickName1"));

var address = person.Address;
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.True);
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False);
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Age"), Is.False);
Assert.That(address.City, Is.EqualTo("City1"));
Assert.That(address.Street, Is.EqualTo("Street1"));
Assert.That(address.PostCode, Is.EqualTo(1001));

var image = person.Image;
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.True);
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Age"), Is.False);
Assert.That(person.Image, Has.Length.EqualTo(1));

var age = person.Age;
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Age"), Is.True);
Assert.That(person.Age, Is.EqualTo(1));
}
}

[TestCase(true)]
[TestCase(false)]
public async Task TestUpdateAsync(bool fetchBeforeUpdate, CancellationToken cancellationToken = default(CancellationToken))
{
Sfi.Statistics.Clear();

using (var s = OpenSession())
using (var tx = s.BeginTransaction())
{
var person = await (s.GetAsync<Person>(1, cancellationToken));
if (fetchBeforeUpdate)
{
var nickName = person.NickName;
}

person.NickName = "test";

await (tx.CommitAsync(cancellationToken));
}

Assert.That(Sfi.Statistics.EntityUpdateCount, Is.EqualTo(1));

using (var s = OpenSession())
using (var tx = s.BeginTransaction())
{
var person = await (s.GetAsync<Person>(1, cancellationToken));
Assert.That(person.NickName, Is.EqualTo("test"));

person.NickName = "NickName1"; // reset name

await (tx.CommitAsync(cancellationToken));
}
}

[Test]
public async Task TestCacheAsync()
{
var persister = Sfi.GetEntityPersister(typeof(Person).FullName);
var cache = (HashtableCache) persister.Cache.Cache;
await (cache.ClearAsync(CancellationToken.None));

using (var s = OpenSession())
using (var tx = s.BeginTransaction())
{
var person = await (s.GetAsync<Person>(1));

var nickName = person.NickName;
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "NickName"), Is.True);
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False);
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False);
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Age"), Is.False);
Assert.That(nickName, Is.EqualTo("NickName1"));

await (tx.CommitAsync());
}

using (var s = OpenSession())
using (var tx = s.BeginTransaction())
{
var person = await (s.GetAsync<Person>(1));
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "NickName"), Is.True);
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False);
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.False);
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Age"), Is.False);
Assert.That(person.NickName, Is.EqualTo("NickName1"));

await (tx.CommitAsync());
}
}

[Test]
public async Task TestInitializeFromCacheAsync()
{
var persister = Sfi.GetEntityPersister(typeof(Person).FullName);
var cache = (HashtableCache) persister.Cache.Cache;
await (cache.ClearAsync(CancellationToken.None));
Sfi.Statistics.Clear();

using (var s = OpenSession())
using (var tx = s.BeginTransaction())
{
var person = await (s.GetAsync<Person>(1));

await (InitializeImageAsync());

Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2));

var image = person.Image; // Should be initialized from cache

Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(2));
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "NickName"), Is.False);
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Address"), Is.False);
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Image"), Is.True);
Assert.That(NHibernateUtil.IsPropertyInitialized(person, "Age"), Is.False);
Assert.That(image, Has.Length.EqualTo(1));

await (tx.CommitAsync());
}
}

private async Task InitializeImageAsync(CancellationToken cancellationToken = default(CancellationToken))
{
using (var s = OpenSession())
using (var tx = s.BeginTransaction())
{
var person = await (s.GetAsync<Person>(1, cancellationToken));
var image = person.Image;

await (tx.CommitAsync(cancellationToken));
}
}

private static Person GeneratePerson(int i)
{
return new Person
{
Id = i,
Name = $"Person{i}",
Address = new Address
{
City = $"City{i}",
PostCode = 1000+i,
Street = $"Street{i}"
},
Image = new byte[i],
NickName = $"NickName{i}"
};

}
}
}
Loading