Skip to content

Commit 4620c94

Browse files
Support IdBag in select id generator
Co-Authored-by: Frédéric Delaporte <12201973+fredericdelaporte@users.noreply.github.com>
1 parent ae61594 commit 4620c94

File tree

14 files changed

+477
-76
lines changed

14 files changed

+477
-76
lines changed

src/NHibernate.Test/Async/NHSpecificTest/NH3150/SelectGeneratorFixture.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
//------------------------------------------------------------------------------
99

1010

11+
using System.Collections.Generic;
1112
using NHibernate.Dialect;
1213
using NUnit.Framework;
1314

@@ -28,6 +29,8 @@ protected override void OnTearDown()
2829
using (var s = OpenSession())
2930
using (var tx = s.BeginTransaction())
3031
{
32+
// Delete loads in memory and handle the cascade.
33+
s.Delete("from Worker2");
3134
s.CreateQuery("delete from System.Object").ExecuteUpdate();
3235
tx.Commit();
3336
}
@@ -117,5 +120,34 @@ public async Task CanUseComponentAsNaturalIdAsync()
117120
Assert.That(worker2.Id, Is.EqualTo(2), "Id of second worker should be 2");
118121
}
119122
}
123+
124+
[Test]
125+
public async Task IdBagWithSelectPOIDAsync()
126+
{
127+
int workerId;
128+
129+
using (var s = OpenSession())
130+
using (var tx = s.BeginTransaction())
131+
{
132+
var worker = new Worker2();
133+
var role = new Role { Description = "keeper" };
134+
135+
worker.Roles = new List<Role>() { role };
136+
137+
await (s.SaveAsync(worker));
138+
await (s.SaveAsync(role));
139+
140+
await (tx.CommitAsync());
141+
142+
workerId = worker.Id;
143+
}
144+
145+
using (var s = OpenSession())
146+
{
147+
var saved_worker = await (s.GetAsync<Worker2>(workerId));
148+
Assert.That(saved_worker.Roles, Is.Not.Null, "roles should not be null");
149+
Assert.That(saved_worker.Roles.Count, Is.EqualTo(1), "roles count should be 1");
150+
}
151+
}
120152
}
121153
}
Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
1-
namespace NHibernate.Test.NHSpecificTest.NH3150
1+
using System.Collections.Generic;
2+
3+
namespace NHibernate.Test.NHSpecificTest.NH3150
24
{
35
public class Worker
46
{
5-
public virtual int? Id { get; set; }
7+
public virtual int Id { get; set; }
68

79
public virtual string Name { get; set; }
810
public virtual string Position { get; set; }
911
}
1012

1113
public class WorkerWithExplicitKey
1214
{
13-
public virtual int? Id { get; set; }
15+
public virtual int Id { get; set; }
1416

1517
public virtual string Name { get; set; }
1618
public virtual string Position { get; set; }
1719
}
1820

1921
public class WorkerWithComponent
2022
{
21-
public virtual int? Id { get; set; }
23+
public virtual int Id { get; set; }
2224
public virtual NidComponent Nid { get; set; }
2325

2426
public class NidComponent
@@ -28,4 +30,16 @@ public class NidComponent
2830
// No need to implement Equals for what the test does.
2931
}
3032
}
33+
34+
public class Worker2
35+
{
36+
public virtual int Id { get; set; }
37+
public virtual IList<Role> Roles { get; set; }
38+
}
39+
40+
public class Role
41+
{
42+
public virtual int Id { get; set; }
43+
public virtual string Description { get; set; }
44+
}
3145
}

src/NHibernate.Test/NHSpecificTest/NH3150/Mappings.hbm.xml

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<?xml version="1.0" encoding="utf-8" ?>
1+
<?xml version="1.0" encoding="utf-8" ?>
22
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
33
namespace="NHibernate.Test.NHSpecificTest.NH3150"
44
assembly="NHibernate.Test">
@@ -37,6 +37,27 @@
3737
</natural-id>
3838
</class>
3939

40+
<class name="Worker2">
41+
<id name="Id">
42+
<generator class="identity"/>
43+
</id>
44+
45+
<idbag name="Roles" inverse="false" cascade="all-delete-orphan" table="workerRoles">
46+
<collection-id column="id" type="Int32">
47+
<generator class="select"/>
48+
</collection-id>
49+
<key column="worker_id"/>
50+
<many-to-many column="role_id" class="Role" fetch="join"/>
51+
</idbag>
52+
</class>
53+
54+
<class name="Role">
55+
<id name="Id">
56+
<generator class="identity"/>
57+
</id>
58+
<property name="Description"/>
59+
</class>
60+
4061
<database-object>
4162
<create>
4263
CREATE TRIGGER dbo.id_gen_Worker ON dbo.Worker
@@ -105,4 +126,28 @@
105126
DROP TRIGGER dbo.id_gen_WorkerWithComponent;
106127
</drop>
107128
</database-object>
129+
130+
<database-object>
131+
<create>
132+
CREATE TRIGGER dbo.id_gen_workerRoles ON dbo.workerRoles
133+
INSTEAD OF INSERT
134+
AS
135+
BEGIN
136+
SET NOCOUNT ON;
137+
138+
declare @lastval int
139+
set @lastval = (select max(id) from workerRoles)
140+
if @lastval is null set @lastval = 0
141+
142+
SELECT * INTO #Inserted FROM Inserted
143+
UPDATE #Inserted set id = @lastval+1
144+
SET NOCOUNT OFF;
145+
INSERT INTO workerRoles SELECT * FROM #Inserted
146+
END
147+
GO
148+
</create>
149+
<drop>
150+
DROP TRIGGER dbo.id_gen_workerRoles;
151+
</drop>
152+
</database-object>
108153
</hibernate-mapping>

src/NHibernate.Test/NHSpecificTest/NH3150/SelectGeneratorFixture.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Generic;
12
using NHibernate.Dialect;
23
using NUnit.Framework;
34

@@ -17,6 +18,8 @@ protected override void OnTearDown()
1718
using (var s = OpenSession())
1819
using (var tx = s.BeginTransaction())
1920
{
21+
// Delete loads in memory and handle the cascade.
22+
s.Delete("from Worker2");
2023
s.CreateQuery("delete from System.Object").ExecuteUpdate();
2124
tx.Commit();
2225
}
@@ -106,5 +109,34 @@ public void CanUseComponentAsNaturalId()
106109
Assert.That(worker2.Id, Is.EqualTo(2), "Id of second worker should be 2");
107110
}
108111
}
112+
113+
[Test]
114+
public void IdBagWithSelectPOID()
115+
{
116+
int workerId;
117+
118+
using (var s = OpenSession())
119+
using (var tx = s.BeginTransaction())
120+
{
121+
var worker = new Worker2();
122+
var role = new Role { Description = "keeper" };
123+
124+
worker.Roles = new List<Role>() { role };
125+
126+
s.Save(worker);
127+
s.Save(role);
128+
129+
tx.Commit();
130+
131+
workerId = worker.Id;
132+
}
133+
134+
using (var s = OpenSession())
135+
{
136+
var saved_worker = s.Get<Worker2>(workerId);
137+
Assert.That(saved_worker.Roles, Is.Not.Null, "roles should not be null");
138+
Assert.That(saved_worker.Roles.Count, Is.EqualTo(1), "roles count should be 1");
139+
}
140+
}
109141
}
110142
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//------------------------------------------------------------------------------
2+
// <auto-generated>
3+
// This code was generated by AsyncGenerator.
4+
//
5+
// Changes to this file may cause incorrect behavior and will be lost if
6+
// the code is regenerated.
7+
// </auto-generated>
8+
//------------------------------------------------------------------------------
9+
10+
11+
using System;
12+
using System.Data.Common;
13+
using System.Linq;
14+
using NHibernate.Engine;
15+
using NHibernate.Id.Insert;
16+
using NHibernate.Persister.Collection;
17+
using NHibernate.Persister.Entity;
18+
using NHibernate.SqlCommand;
19+
using NHibernate.Type;
20+
21+
namespace NHibernate.Id
22+
{
23+
using System.Threading.Tasks;
24+
using System.Threading;
25+
26+
public partial interface ICompositeKeyPostInsertIdentityPersister
27+
{
28+
29+
/// <summary>
30+
/// Bind the parameter values of a SQL select command that performs a select based on an unique key.
31+
/// </summary>
32+
/// <param name="session">The current <see cref="ISession" />.</param>
33+
/// <param name="selectCommand">The command.</param>
34+
/// <param name="binder">The id insertion binder.</param>
35+
/// <param name="suppliedPropertyNames">The names of the properties which map to the column(s) to use
36+
/// in the select statement restriction. If supplied, they override the persister logic for determining
37+
/// them.</param>
38+
/// <param name="cancellationToken">A cancellation token that can be used to cancel the work</param>
39+
/// <exception cref="NotSupportedException">thrown if <paramref name="suppliedPropertyNames"/> are
40+
/// specified on a persister which does not allow a custom key.</exception>
41+
Task BindSelectByUniqueKeyAsync(
42+
ISessionImplementor session,
43+
DbCommand selectCommand,
44+
IBinder binder,
45+
string[] suppliedPropertyNames, CancellationToken cancellationToken);
46+
}
47+
}

src/NHibernate/Async/Id/Insert/AbstractSelectingDelegate.cs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
//------------------------------------------------------------------------------
99

1010

11+
using System;
1112
using System.Data;
1213
using System.Data.Common;
1314
using NHibernate.Engine;
@@ -62,7 +63,7 @@ public async Task<object> PerformInsertAsync(SqlCommandInfo insertSql, ISessionI
6263
var idSelect = await (session.Batcher.PrepareCommandAsync(CommandType.Text, selectSql, ParametersTypes, cancellationToken)).ConfigureAwait(false);
6364
try
6465
{
65-
await (BindParametersAsync(session, idSelect, binder.Entity, cancellationToken)).ConfigureAwait(false);
66+
await (BindParametersAsync(session, idSelect, binder, cancellationToken)).ConfigureAwait(false);
6667
var rs = await (session.Batcher.ExecuteReaderAsync(idSelect, cancellationToken)).ConfigureAwait(false);
6768
try
6869
{
@@ -107,6 +108,8 @@ public async Task<object> PerformInsertAsync(SqlCommandInfo insertSql, ISessionI
107108
/// <param name="ps">The prepared <see cref="SelectSQL"/> command </param>
108109
/// <param name="entity">The entity being saved. </param>
109110
/// <param name="cancellationToken">A cancellation token that can be used to cancel the work</param>
111+
// Since 5.2
112+
[Obsolete("Use or override BindParameters(ISessionImplementor session, DbCommand ps, IBinder binder) instead.")]
110113
protected internal virtual Task BindParametersAsync(ISessionImplementor session, DbCommand ps, object entity, CancellationToken cancellationToken)
111114
{
112115
if (cancellationToken.IsCancellationRequested)
@@ -118,10 +121,27 @@ protected internal virtual Task BindParametersAsync(ISessionImplementor session,
118121
BindParameters(session, ps, entity);
119122
return Task.CompletedTask;
120123
}
121-
catch (System.Exception ex)
124+
catch (Exception ex)
122125
{
123126
return Task.FromException<object>(ex);
124127
}
125128
}
129+
130+
/// <summary>Bind any required parameter values into the SQL command <see cref="SelectSQL"/>.</summary>
131+
/// <param name="session">The session.</param>
132+
/// <param name="ps">The prepared <see cref="SelectSQL"/> command.</param>
133+
/// <param name="binder">The binder for the entity or collection being saved.</param>
134+
/// <param name="cancellationToken">A cancellation token that can be used to cancel the work</param>
135+
protected internal virtual Task BindParametersAsync(ISessionImplementor session, DbCommand ps, IBinder binder, CancellationToken cancellationToken)
136+
{
137+
if (cancellationToken.IsCancellationRequested)
138+
{
139+
return Task.FromCanceled<object>(cancellationToken);
140+
}
141+
// 6.0 TODO: remove the call to the obsoleted method.
142+
#pragma warning disable 618
143+
return BindParametersAsync(session, ps, binder.Entity, cancellationToken);
144+
#pragma warning restore 618
145+
}
126146
}
127147
}

src/NHibernate/Async/Id/SelectGenerator.cs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
//------------------------------------------------------------------------------
99

1010

11+
using System;
1112
using System.Collections.Generic;
1213
using System.Data.Common;
1314
using System.Linq;
@@ -30,23 +31,52 @@ public partial class SelectGenerator : AbstractPostInsertGenerator, IConfigurabl
3031
public partial class SelectGeneratorDelegate : AbstractSelectingDelegate
3132
{
3233

34+
// Since 5.2
35+
[Obsolete("Use or override BindParameters(ISessionImplementor session, DbCommand ps, IBinder binder) instead.")]
3336
protected internal override async Task BindParametersAsync(ISessionImplementor session, DbCommand ps, object entity, CancellationToken cancellationToken)
3437
{
3538
cancellationToken.ThrowIfCancellationRequested();
39+
var entityPersister = (IEntityPersister) persister;
40+
var uniqueKeyPropertyNames = uniqueKeySuppliedPropertyNames ??
41+
PostInsertIdentityPersisterExtension.DetermineNameOfPropertiesToUse(entityPersister);
3642
for (var i = 0; i < uniqueKeyPropertyNames.Length; i++)
3743
{
3844
var uniqueKeyValue = entityPersister.GetPropertyValue(entity, uniqueKeyPropertyNames[i]);
3945
await (uniqueKeyTypes[i].NullSafeSetAsync(ps, uniqueKeyValue, i, session, cancellationToken)).ConfigureAwait(false);
4046
}
4147
}
4248

49+
protected internal override async Task BindParametersAsync(ISessionImplementor session, DbCommand ps, IBinder binder, CancellationToken cancellationToken)
50+
{
51+
cancellationToken.ThrowIfCancellationRequested();
52+
// 6.0 TODO: remove the "if" block and do the other TODO of this method
53+
if (persister is IEntityPersister)
54+
{
55+
#pragma warning disable 618
56+
await (BindParametersAsync(session, ps, binder.Entity, cancellationToken)).ConfigureAwait(false);
57+
#pragma warning restore 618
58+
return;
59+
}
60+
61+
if (persister is ICompositeKeyPostInsertIdentityPersister compositeKeyPersister)
62+
{
63+
await (compositeKeyPersister.BindSelectByUniqueKeyAsync(session, ps, binder, uniqueKeySuppliedPropertyNames, cancellationToken)).ConfigureAwait(false);
64+
return;
65+
}
66+
67+
// 6.0 TODO: remove by merging ICompositeKeyPostInsertIdentityPersister in IPostInsertIdentityPersister
68+
await (binder.BindValuesAsync(ps, cancellationToken)).ConfigureAwait(false);
69+
}
70+
4371
protected internal override async Task<object> GetResultAsync(ISessionImplementor session, DbDataReader rs, object entity, CancellationToken cancellationToken)
4472
{
4573
cancellationToken.ThrowIfCancellationRequested();
4674
if (!await (rs.ReadAsync(cancellationToken)).ConfigureAwait(false))
4775
{
48-
throw new IdentifierGenerationException(
49-
$"The inserted row could not be located by the unique key: {string.Join(", ", uniqueKeyPropertyNames)}");
76+
var message = "The inserted row could not be located by the unique key";
77+
if (uniqueKeySuppliedPropertyNames != null)
78+
message = $"{message} (supplied unique key: {string.Join(", ", uniqueKeySuppliedPropertyNames)})";
79+
throw new IdentifierGenerationException(message);
5080
}
5181
return await (idType.NullSafeGetAsync(rs, persister.RootTableKeyColumnNames, session, entity, cancellationToken)).ConfigureAwait(false);
5282
}

src/NHibernate/Async/Persister/Collection/AbstractCollectionPersister.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,21 @@ protected async Task<object> PerformInsertAsync(object ownerId, IPersistentColle
555555

556556
#region NH specific
557557

558+
#region IPostInsertIdentityPersister Members
559+
560+
public Task BindSelectByUniqueKeyAsync(
561+
ISessionImplementor session,
562+
DbCommand selectCommand,
563+
IBinder binder,
564+
string[] suppliedPropertyNames, CancellationToken cancellationToken)
565+
{
566+
if (cancellationToken.IsCancellationRequested)
567+
{
568+
return Task.FromCanceled<object>(cancellationToken);
569+
}
570+
return binder.BindValuesAsync(selectCommand, cancellationToken);
571+
}
572+
#endregion
558573

559574
/// <summary>
560575
/// Perform an SQL INSERT, and then retrieve a generated identifier.

0 commit comments

Comments
 (0)