Skip to content

Use entities prepared by Loader in hql select projections #2082

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 6 commits into from
Apr 5, 2020
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//------------------------------------------------------------------------------
// <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.Linq;
using NHibernate.Cfg.MappingSchema;
using NHibernate.Mapping.ByCode;
using NUnit.Framework;
using NHibernate.Linq;

namespace NHibernate.Test.NHSpecificTest.GH2064
{
using System.Threading.Tasks;
[TestFixture]
public class OneToOneSelectProjectionFixtureAsync : TestCaseMappingByCode
{
protected override HbmMapping GetMappings()
{
var mapper = new ModelMapper();
mapper.Class<OneToOneEntity>(
rc =>
{
rc.Id(e => e.Id, m => m.Generator(Generators.Assigned));
rc.Property(e => e.Name);
});

mapper.Class<ParentEntity>(
rc =>
{
rc.Id(e => e.Id, m => m.Generator(Generators.GuidComb));
rc.Property(e => e.Name);
rc.OneToOne(e => e.OneToOne, m => { });
});

return mapper.CompileMappingForAllExplicitlyAddedEntities();
}

protected override void OnSetUp()
{
using (var session = OpenSession())
using (var transaction = session.BeginTransaction())
{
var nullableOwner = new ParentEntity() {Name = "Owner",};
var oneToOne = new OneToOneEntity() {Name = "OneToOne"};
nullableOwner.OneToOne = oneToOne;
session.Save(nullableOwner);
oneToOne.Id = nullableOwner.Id;
session.Save(oneToOne);
session.Flush();

transaction.Commit();
}
}

protected override void OnTearDown()
{
using (var session = OpenSession())
using (var transaction = session.BeginTransaction())
{
// The HQL delete does all the job inside the database without loading the entities, but it does
// not handle delete order for avoiding violating constraints if any. Use
// session.Delete("from System.Object");
// instead if in need of having NHibernate ordering the deletes, but this will cause
// loading the entities in the session.
session.CreateQuery("delete from System.Object").ExecuteUpdate();

transaction.Commit();
}
}

[Test]
public async Task QueryOneToOneAsync()
{
using (var session = OpenSession())
{
var entity =
await (session
.Query<ParentEntity>()
.FirstOrDefaultAsync());
Assert.That(entity.OneToOne, Is.Not.Null);
}
}

[Test]
public async Task QueryOneToOneProjectionAsync()
{
using (var session = OpenSession())
{
var entity =
await (session
.Query<ParentEntity>()
.Select(
x => new
{
x.Id,
SubType = new {x.OneToOne, x.Name},
SubType2 = new
{
x.Id,
x.OneToOne,
SubType3 = new {x.Id, x.OneToOne}
},
x.OneToOne
}).FirstOrDefaultAsync());
Assert.That(entity.OneToOne, Is.Not.Null);
Assert.That(entity.SubType.OneToOne, Is.Not.Null);
Assert.That(entity.SubType2.OneToOne, Is.Not.Null);
Assert.That(entity.SubType2.SubType3.OneToOne, Is.Not.Null);
}
}
}
}
10 changes: 10 additions & 0 deletions src/NHibernate.Test/NHSpecificTest/GH2064/OneToOneEntity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;

namespace NHibernate.Test.NHSpecificTest.GH2064
{
public class OneToOneEntity
{
public virtual Guid Id { get; set; }
public virtual string Name { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using System.Linq;
using NHibernate.Cfg.MappingSchema;
using NHibernate.Mapping.ByCode;
using NUnit.Framework;

namespace NHibernate.Test.NHSpecificTest.GH2064
{
[TestFixture]
public class OneToOneSelectProjectionFixture : TestCaseMappingByCode
{
protected override HbmMapping GetMappings()
{
var mapper = new ModelMapper();
mapper.Class<OneToOneEntity>(
rc =>
{
rc.Id(e => e.Id, m => m.Generator(Generators.Assigned));
rc.Property(e => e.Name);
});

mapper.Class<ParentEntity>(
rc =>
{
rc.Id(e => e.Id, m => m.Generator(Generators.GuidComb));
rc.Property(e => e.Name);
rc.OneToOne(e => e.OneToOne, m => { });
});

return mapper.CompileMappingForAllExplicitlyAddedEntities();
}

protected override void OnSetUp()
{
using (var session = OpenSession())
using (var transaction = session.BeginTransaction())
{
var nullableOwner = new ParentEntity() {Name = "Owner",};
var oneToOne = new OneToOneEntity() {Name = "OneToOne"};
nullableOwner.OneToOne = oneToOne;
session.Save(nullableOwner);
oneToOne.Id = nullableOwner.Id;
session.Save(oneToOne);
session.Flush();

transaction.Commit();
}
}

protected override void OnTearDown()
{
using (var session = OpenSession())
using (var transaction = session.BeginTransaction())
{
// The HQL delete does all the job inside the database without loading the entities, but it does
// not handle delete order for avoiding violating constraints if any. Use
// session.Delete("from System.Object");
// instead if in need of having NHibernate ordering the deletes, but this will cause
// loading the entities in the session.
session.CreateQuery("delete from System.Object").ExecuteUpdate();

transaction.Commit();
}
}

[Test]
public void QueryOneToOne()
{
using (var session = OpenSession())
{
var entity =
session
.Query<ParentEntity>()
.FirstOrDefault();
Assert.That(entity.OneToOne, Is.Not.Null);
}
}

[Test]
public void QueryOneToOneProjection()
{
using (var session = OpenSession())
{
var entity =
session
.Query<ParentEntity>()
.Select(
x => new
{
x.Id,
SubType = new {x.OneToOne, x.Name},
SubType2 = new
{
x.Id,
x.OneToOne,
SubType3 = new {x.Id, x.OneToOne}
},
x.OneToOne
}).FirstOrDefault();
Assert.That(entity.OneToOne, Is.Not.Null);
Assert.That(entity.SubType.OneToOne, Is.Not.Null);
Assert.That(entity.SubType2.OneToOne, Is.Not.Null);
Assert.That(entity.SubType2.SubType3.OneToOne, Is.Not.Null);
}
}
}
}
11 changes: 11 additions & 0 deletions src/NHibernate.Test/NHSpecificTest/GH2064/ParentEntity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;

namespace NHibernate.Test.NHSpecificTest.GH2064
{
public class ParentEntity
{
public virtual Guid Id { get; set; }
public virtual string Name { get; set; }
public virtual OneToOneEntity OneToOne { get; set; }
}
}
4 changes: 3 additions & 1 deletion src/NHibernate/Async/Loader/Hql/QueryLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ protected override async Task<object[]> GetResultRowAsync(object[] row, DbDataRe
resultRow = new object[queryCols];
for (int i = 0; i < queryCols; i++)
{
resultRow[i] = await (ResultTypes[i].NullSafeGetAsync(rs, scalarColumns[i], session, null, cancellationToken)).ConfigureAwait(false);
resultRow[i] = _entityByResultTypeDic.TryGetValue(i, out var rowIndex)
? row[rowIndex]
: await (ResultTypes[i].NullSafeGetAsync(rs, scalarColumns[i], session, null, cancellationToken)).ConfigureAwait(false);
}
}
else
Expand Down
7 changes: 0 additions & 7 deletions src/NHibernate/Hql/Ast/ANTLR/Tree/ConstructorNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,6 @@ public void Prepare()
private IType[] ResolveConstructorArgumentTypes()
{
ISelectExpression[] argumentExpressions = CollectSelectExpressions();

if ( argumentExpressions == null )
{
// return an empty Type array
return Array.Empty<IType>();
}

IType[] types = new IType[argumentExpressions.Length];
for ( int x = 0; x < argumentExpressions.Length; x++ )
{
Expand Down
31 changes: 22 additions & 9 deletions src/NHibernate/Hql/Ast/ANTLR/Tree/SelectClause.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public class SelectClause : SelectExpressionList
private IType[] _queryReturnTypes;
private string[][] _columnNames;
private readonly List<FromElement> _fromElementsForLoad = new List<FromElement>();
private readonly Dictionary<int, int> _entityByResultTypeDic = new Dictionary<int, int>();

private ConstructorNode _constructorNode;
private string[] _aliases;
private int[] _columnNamesStartPositions;
Expand Down Expand Up @@ -134,19 +136,20 @@ public void InitializeExplicitSelectClause(FromClause fromClause)
if (expr.IsConstructor)
{
_constructorNode = (ConstructorNode)expr;
IList<IType> constructorArgumentTypeList = _constructorNode.ConstructorArgumentTypeList;
//sqlResultTypeList.addAll( constructorArgumentTypeList );
queryReturnTypeList.AddRange(constructorArgumentTypeList);
_scalarSelect = true;

for (int j = 1; j < _constructorNode.ChildCount; j++)
var ctorSelectExpressions = _constructorNode.CollectSelectExpressions();
for (int j = 0; j < ctorSelectExpressions.Length; j++)
{
ISelectExpression se = _constructorNode.GetChild(j) as ISelectExpression;
ISelectExpression se = ctorSelectExpressions[j];

if (se != null && IsReturnableEntity(se))
if (IsReturnableEntity(se))
{
_fromElementsForLoad.Add(se.FromElement);
AddEntityToProjection(queryReturnTypeList.Count, se);
}

queryReturnTypeList.Add(se.DataType);
}
}
else
Expand All @@ -163,10 +166,9 @@ public void InitializeExplicitSelectClause(FromClause fromClause)
{
_scalarSelect = true;
}

if (IsReturnableEntity(expr))
else if (IsReturnableEntity(expr))
{
_fromElementsForLoad.Add(expr.FromElement);
AddEntityToProjection(queryReturnTypeList.Count, expr);
}

// Always add the type to the return type list.
Expand Down Expand Up @@ -247,6 +249,12 @@ public void InitializeExplicitSelectClause(FromClause fromClause)
FinishInitialization( /*sqlResultTypeList,*/ queryReturnTypeList);
}

private void AddEntityToProjection(int resultIndex, ISelectExpression se)
{
_entityByResultTypeDic[resultIndex] = _fromElementsForLoad.Count;
_fromElementsForLoad.Add(se.FromElement);
}

private static FromElement GetOrigin(FromElement fromElement)
{
var realOrigin = fromElement.RealOrigin;
Expand All @@ -271,6 +279,11 @@ public IList<FromElement> FromElementsForLoad
get { return _fromElementsForLoad; }
}

/// <summary>
/// Maps QueryReturnTypes[key] to entities from FromElementsForLoad[value]
/// </summary>
internal IReadOnlyDictionary<int, int> EntityByResultTypeDic => _entityByResultTypeDic;

public bool IsScalarSelect
{
get { return _scalarSelect; }
Expand Down
6 changes: 5 additions & 1 deletion src/NHibernate/Loader/Hql/QueryLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public partial class QueryLoader : BasicLoader
private IType[] _cacheTypes;
private ISet<ICollectionPersister> _uncacheableCollectionPersisters;
private Dictionary<string, string[]>[] _collectionUserProvidedAliases;
private IReadOnlyDictionary<int, int> _entityByResultTypeDic;

public QueryLoader(QueryTranslatorImpl queryTranslator, ISessionFactoryImplementor factory, SelectClause selectClause)
: base(factory)
Expand Down Expand Up @@ -214,6 +215,7 @@ protected override IDictionary<string, string[]> GetCollectionUserProvidedAlias(
private void Initialize(SelectClause selectClause)
{
IList<FromElement> fromElementList = selectClause.FromElementsForLoad;
_entityByResultTypeDic = selectClause.EntityByResultTypeDic;

_hasScalars = selectClause.IsScalarSelect;
_scalarColumnNames = selectClause.ColumnNames;
Expand Down Expand Up @@ -401,7 +403,9 @@ protected override object[] GetResultRow(object[] row, DbDataReader rs, ISession
resultRow = new object[queryCols];
for (int i = 0; i < queryCols; i++)
{
resultRow[i] = ResultTypes[i].NullSafeGet(rs, scalarColumns[i], session, null);
resultRow[i] = _entityByResultTypeDic.TryGetValue(i, out var rowIndex)
? row[rowIndex]
: ResultTypes[i].NullSafeGet(rs, scalarColumns[i], session, null);
}
}
else
Expand Down