Skip to content

Fix many-to-many fetch issue #3316

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 8 commits into from
Jun 14, 2023
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
147 changes: 147 additions & 0 deletions src/NHibernate.Test/Async/NHSpecificTest/GH3290/Fixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
//------------------------------------------------------------------------------
// <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 NHibernate.Cfg;
using NHibernate.Cfg.MappingSchema;
using NHibernate.Mapping.ByCode;
using NHibernate.Transform;
using NUnit.Framework;

namespace NHibernate.Test.NHSpecificTest.GH3290
{
using System.Threading.Tasks;
[TestFixture(true)]
[TestFixture(false)]
public class FixtureAsync : TestCaseMappingByCode
{
private readonly bool _detectFetchLoops;

public FixtureAsync(bool detectFetchLoops)
{
_detectFetchLoops = detectFetchLoops;
}

protected override HbmMapping GetMappings()
{
var mapper = new ModelMapper();
mapper.Class<Entity>(rc =>
{
rc.Id(x => x.Id, map => map.Generator(Generators.GuidComb));

rc.Property(
x => x.Name
);

rc.Set(
x => x.Children,
v =>
{
v.Table("EntityToEntity");
v.Cascade(Mapping.ByCode.Cascade.None);
v.Inverse(true);
v.Key(x =>
{
x.Column("ParentId");
x.NotNullable(true);
});
v.Lazy(CollectionLazy.Lazy);
v.Fetch(CollectionFetchMode.Join);
},
h => h.ManyToMany(m => m.Column("ChildId"))
);

rc.Set(
x => x.Parents,
v =>
{
v.Table("EntityToEntity");
v.Cascade(Mapping.ByCode.Cascade.All);

v.Key(x =>
{
x.Column("ChildId");
x.NotNullable(true);
});
v.Lazy(CollectionLazy.Lazy);
v.Fetch(CollectionFetchMode.Join);
},
h => h.ManyToMany(m => m.Column("ParentId"))
);
});

return mapper.CompileMappingForAllExplicitlyAddedEntities();
}

protected override void Configure(Configuration configuration)
{
configuration.SetProperty(Environment.DetectFetchLoops, _detectFetchLoops ? "true" : "false");
configuration.SetProperty(Environment.MaxFetchDepth, _detectFetchLoops ? "-1" : "2");
}

protected override void OnSetUp()
{
using var session = OpenSession();
using var transaction = session.BeginTransaction();

var person = new Entity
{
Name = "pers",
Parents = new HashSet<Entity>()
};
session.Save(person);
var job = new Entity
{
Name = "job",
Children = new HashSet<Entity>()
};
session.Save(job);

job.Children.Add(person);
person.Parents.Add(job);

transaction.Commit();
}

protected override void OnTearDown()
{
using var session = OpenSession();
using var transaction = session.BeginTransaction();

session.CreateSQLQuery("delete from EntityToEntity").ExecuteUpdate();
session.CreateQuery("delete from System.Object").ExecuteUpdate();

transaction.Commit();
}

[Test]
public async Task QueryWithFetchAsync()
{
using var session = OpenSession();
using var _ = session.BeginTransaction();

var all = await (session
.QueryOver<Entity>()
.Fetch(SelectMode.Fetch, x => x.Children)
.Fetch(SelectMode.Fetch, x => x.Parents)
.TransformUsing(Transformers.DistinctRootEntity)
.ListAsync());

foreach (var entity in all)
{
var isPerson = entity.Name == "pers";
if (isPerson)
Assert.That(entity.Parents, Has.Count.EqualTo(1), "Person's job not found or non-unique.");
else
Assert.That(entity.Children, Has.Count.EqualTo(1), "Job's employee not found or non-unique.");
}
}
}
}
4 changes: 3 additions & 1 deletion src/NHibernate.Test/NHSpecificTest/GH3288/Entity.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;

namespace NHibernate.Test.NHSpecificTest.GH3288
{
Expand All @@ -10,6 +11,7 @@ class TopEntity
class MiddleEntity
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual ISet<Component> Components { get; set; } = new HashSet<Component>();
}

Expand Down
1 change: 1 addition & 0 deletions src/NHibernate.Test/NHSpecificTest/GH3288/Mappings.hbm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<id name="Id" >
<generator class="identity" />
</id>
<property name="Name" />
<set cascade="all" name="Components" table="BottomEntity">
<key not-null="true">
<column name="MiddleEntity_id" />
Expand Down
13 changes: 13 additions & 0 deletions src/NHibernate.Test/NHSpecificTest/GH3290/Entity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;

namespace NHibernate.Test.NHSpecificTest.GH3290
{
class Entity
{
public virtual Guid Id { get; set; }
public virtual string Name { get; set; }
public virtual ISet<Entity> Parents { get; set; }
public virtual ISet<Entity> Children { get; set; }
}
}
136 changes: 136 additions & 0 deletions src/NHibernate.Test/NHSpecificTest/GH3290/Fixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using System.Collections.Generic;
using NHibernate.Cfg;
using NHibernate.Cfg.MappingSchema;
using NHibernate.Mapping.ByCode;
using NHibernate.Transform;
using NUnit.Framework;

namespace NHibernate.Test.NHSpecificTest.GH3290
{
[TestFixture(true)]
[TestFixture(false)]
public class Fixture : TestCaseMappingByCode
{
private readonly bool _detectFetchLoops;

public Fixture(bool detectFetchLoops)
{
_detectFetchLoops = detectFetchLoops;
}

protected override HbmMapping GetMappings()
{
var mapper = new ModelMapper();
mapper.Class<Entity>(rc =>
{
rc.Id(x => x.Id, map => map.Generator(Generators.GuidComb));

rc.Property(
x => x.Name
);

rc.Set(
x => x.Children,
v =>
{
v.Table("EntityToEntity");
v.Cascade(Mapping.ByCode.Cascade.None);
v.Inverse(true);
v.Key(x =>
{
x.Column("ParentId");
x.NotNullable(true);
});
v.Lazy(CollectionLazy.Lazy);
v.Fetch(CollectionFetchMode.Join);
},
h => h.ManyToMany(m => m.Column("ChildId"))
);

rc.Set(
x => x.Parents,
v =>
{
v.Table("EntityToEntity");
v.Cascade(Mapping.ByCode.Cascade.All);

v.Key(x =>
{
x.Column("ChildId");
x.NotNullable(true);
});
v.Lazy(CollectionLazy.Lazy);
v.Fetch(CollectionFetchMode.Join);
},
h => h.ManyToMany(m => m.Column("ParentId"))
);
});

return mapper.CompileMappingForAllExplicitlyAddedEntities();
}

protected override void Configure(Configuration configuration)
{
configuration.SetProperty(Environment.DetectFetchLoops, _detectFetchLoops ? "true" : "false");
configuration.SetProperty(Environment.MaxFetchDepth, _detectFetchLoops ? "-1" : "2");
}

protected override void OnSetUp()
{
using var session = OpenSession();
using var transaction = session.BeginTransaction();

var person = new Entity
{
Name = "pers",
Parents = new HashSet<Entity>()
};
session.Save(person);
var job = new Entity
{
Name = "job",
Children = new HashSet<Entity>()
};
session.Save(job);

job.Children.Add(person);
person.Parents.Add(job);

transaction.Commit();
}

protected override void OnTearDown()
{
using var session = OpenSession();
using var transaction = session.BeginTransaction();

session.CreateSQLQuery("delete from EntityToEntity").ExecuteUpdate();
session.CreateQuery("delete from System.Object").ExecuteUpdate();

transaction.Commit();
}

[Test]
public void QueryWithFetch()
{
using var session = OpenSession();
using var _ = session.BeginTransaction();

var all = session
.QueryOver<Entity>()
.Fetch(SelectMode.Fetch, x => x.Children)
.Fetch(SelectMode.Fetch, x => x.Parents)
.TransformUsing(Transformers.DistinctRootEntity)
.List();

foreach (var entity in all)
{
var isPerson = entity.Name == "pers";
if (isPerson)
Assert.That(entity.Parents, Has.Count.EqualTo(1), "Person's job not found or non-unique.");
else
Assert.That(entity.Children, Has.Count.EqualTo(1), "Job's employee not found or non-unique.");
}
}
}
}
10 changes: 9 additions & 1 deletion src/NHibernate/Loader/JoinWalker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,15 @@ private void AddAssociationToJoinTree(IAssociationType type, string[] aliasedLhs

if (qc != null)
{
_joinQueue.Enqueue(new CollectionJoinQueueEntry(qc, subalias, path, pathAlias));
var collection = new CollectionJoinQueueEntry(qc, subalias, path, pathAlias);
// Many-to-Many element entity join needs to be added right after collection bridge table
// (see IsManyToManyWith, ManyToManySelectFragment, IsManyToManyRoot usages)
if (qc.IsManyToMany)
{
collection.Walk(this);
return;
}
_joinQueue.Enqueue(collection);
}
else if (joinable is IOuterJoinLoadable jl)
{
Expand Down