Skip to content

Commit 05264ba

Browse files
maca88fredericDelaporte
authored andcommitted
Add a generic batcher for insert/update/delete statements, usable with PostgreSQL and others (#1588)
Co-authored-by: Frédéric Delaporte <fredericDelaporte@users.noreply.github.com>
1 parent 9514cd2 commit 05264ba

16 files changed

+838
-13
lines changed

src/NHibernate.Test/Ado/BatcherFixture.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ private void FillDb()
5959
{
6060
s.Save(new VerySimple {Id = 1, Name = "Fabio", Weight = 119.5});
6161
s.Save(new VerySimple {Id = 2, Name = "Fiamma", Weight = 9.8});
62+
s.Save(new VerySimple {Id = 3, Name = "Roberto", Weight = 98.8 });
6263
tx.Commit();
6364
}
6465
}
@@ -74,11 +75,14 @@ public void OneRoundTripUpdate()
7475
{
7576
var vs1 = s.Get<VerySimple>(1);
7677
var vs2 = s.Get<VerySimple>(2);
78+
var vs3 = s.Get<VerySimple>(3);
7779
vs1.Weight -= 10;
7880
vs2.Weight -= 1;
81+
vs3.Weight -= 5;
7982
Sfi.Statistics.Clear();
8083
s.Update(vs1);
8184
s.Update(vs2);
85+
s.Update(vs3);
8286
tx.Commit();
8387
}
8488

@@ -154,9 +158,11 @@ public void OneRoundTripDelete()
154158
{
155159
var vs1 = s.Get<VerySimple>(1);
156160
var vs2 = s.Get<VerySimple>(2);
161+
var vs3 = s.Get<VerySimple>(3);
157162
Sfi.Statistics.Clear();
158163
s.Delete(vs1);
159164
s.Delete(vs2);
165+
s.Delete(vs3);
160166
tx.Commit();
161167
}
162168

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
using System;
2+
using System.Collections;
3+
using System.Diagnostics;
4+
using System.Linq;
5+
using NHibernate.AdoNet;
6+
using NHibernate.Cfg;
7+
using NHibernate.Dialect;
8+
using NUnit.Framework;
9+
using Environment = NHibernate.Cfg.Environment;
10+
11+
namespace NHibernate.Test.Ado
12+
{
13+
[TestFixture]
14+
public class GenericBatchingBatcherFixture : TestCase
15+
{
16+
protected override string MappingsAssembly => "NHibernate.Test";
17+
18+
protected override IList Mappings => new[] {"Ado.VerySimple.hbm.xml"};
19+
20+
protected override void Configure(Configuration configuration)
21+
{
22+
configuration.SetProperty(Environment.BatchStrategy, typeof(GenericBatchingBatcherFactory).AssemblyQualifiedName);
23+
configuration.SetProperty(Environment.GenerateStatistics, "true");
24+
configuration.SetProperty(Environment.BatchSize, "1000");
25+
}
26+
27+
protected override bool AppliesTo(Dialect.Dialect dialect)
28+
{
29+
return !(dialect is FirebirdDialect) &&
30+
!(dialect is Oracle8iDialect) &&
31+
!(dialect is MsSqlCeDialect);
32+
}
33+
34+
[Test]
35+
public void MassiveInsertUpdateDeleteTest()
36+
{
37+
var totalRecords = 1000;
38+
BatchInsert(totalRecords);
39+
BatchUpdate(totalRecords);
40+
BatchDelete(totalRecords);
41+
42+
DbShoudBeEmpty();
43+
}
44+
45+
[Test]
46+
public void BatchSizeTest()
47+
{
48+
using (var sqlLog = new SqlLogSpy())
49+
using (var s = Sfi.OpenSession())
50+
using (var tx = s.BeginTransaction())
51+
{
52+
s.SetBatchSize(5);
53+
for (var i = 0; i < 20; i++)
54+
{
55+
s.Save(new VerySimple { Id = 1 + i, Name = $"Fabio{i}", Weight = 1.45 + i });
56+
}
57+
tx.Commit();
58+
59+
var log = sqlLog.GetWholeLog();
60+
Assert.That(FindAllOccurrences(log, "Batch commands:"), Is.EqualTo(4));
61+
}
62+
Cleanup();
63+
}
64+
65+
// Demonstrates a 50% performance gain with SQL-Server, around 40% for PostgreSQL,
66+
// around 15% for MySql, but around 200% performance loss for SQLite.
67+
// (Tested with databases on same machine for all cases.)
68+
[Theory, Explicit("This is a performance test, to be checked manually.")]
69+
public void MassivePerformanceTest(bool batched)
70+
{
71+
if (batched)
72+
{
73+
// Bring down batch size to a reasonnable value, otherwise performances are worsen.
74+
cfg.SetProperty(Environment.BatchSize, "50");
75+
}
76+
else
77+
{
78+
cfg.SetProperty(Environment.BatchStrategy, typeof(NonBatchingBatcherFactory).AssemblyQualifiedName);
79+
cfg.Properties.Remove(Environment.BatchSize);
80+
}
81+
RebuildSessionFactory();
82+
83+
try
84+
{
85+
// Warm up
86+
MassiveInsertUpdateDeleteTest();
87+
88+
var chrono = new Stopwatch();
89+
chrono.Start();
90+
MassiveInsertUpdateDeleteTest();
91+
Console.WriteLine($"Elapsed time: {chrono.Elapsed}");
92+
}
93+
finally
94+
{
95+
Configure(cfg);
96+
RebuildSessionFactory();
97+
}
98+
}
99+
100+
private void BatchInsert(int totalRecords)
101+
{
102+
Sfi.Statistics.Clear();
103+
using (var s = Sfi.OpenSession())
104+
using (var tx = s.BeginTransaction())
105+
{
106+
for (var i = 0; i < totalRecords; i++)
107+
{
108+
s.Save(new VerySimple {Id = 1 + i, Name = $"Fabio{i}", Weight = 1.45 + i});
109+
}
110+
tx.Commit();
111+
}
112+
Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1));
113+
}
114+
115+
public void BatchUpdate(int totalRecords)
116+
{
117+
using (var s = Sfi.OpenSession())
118+
using (var tx = s.BeginTransaction())
119+
{
120+
var items = s.Query<VerySimple>().ToList();
121+
Assert.That(items.Count, Is.EqualTo(totalRecords));
122+
123+
foreach (var item in items)
124+
{
125+
item.Weight += 5;
126+
s.Update(item);
127+
}
128+
129+
Sfi.Statistics.Clear();
130+
tx.Commit();
131+
}
132+
Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1));
133+
}
134+
135+
public void BatchDelete(int totalRecords)
136+
{
137+
using (var s = Sfi.OpenSession())
138+
using (var tx = s.BeginTransaction())
139+
{
140+
var items = s.Query<VerySimple>().ToList();
141+
Assert.That(items.Count, Is.EqualTo(totalRecords));
142+
143+
foreach (var item in items)
144+
{
145+
s.Delete(item);
146+
}
147+
148+
Sfi.Statistics.Clear();
149+
tx.Commit();
150+
}
151+
Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1));
152+
}
153+
154+
private void DbShoudBeEmpty()
155+
{
156+
using (var s = Sfi.OpenSession())
157+
using (var tx = s.BeginTransaction())
158+
{
159+
var items = s.Query<VerySimple>().ToList();
160+
Assert.That(items.Count, Is.EqualTo(0));
161+
162+
tx.Commit();
163+
}
164+
}
165+
166+
private void Cleanup()
167+
{
168+
using (var s = Sfi.OpenSession())
169+
using (s.BeginTransaction())
170+
{
171+
s.CreateQuery("delete from VerySimple").ExecuteUpdate();
172+
s.Transaction.Commit();
173+
}
174+
}
175+
176+
private int FindAllOccurrences(string source, string substring)
177+
{
178+
if (source == null)
179+
{
180+
return 0;
181+
}
182+
int n = 0, count = 0;
183+
while ((n = source.IndexOf(substring, n, StringComparison.InvariantCulture)) != -1)
184+
{
185+
n += substring.Length;
186+
++count;
187+
}
188+
return count;
189+
}
190+
}
191+
}

src/NHibernate.Test/Async/Ado/BatcherFixture.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ public async Task OneRoundTripInsertsAsync()
7171
{
7272
await (s.SaveAsync(new VerySimple {Id = 1, Name = "Fabio", Weight = 119.5}, cancellationToken));
7373
await (s.SaveAsync(new VerySimple {Id = 2, Name = "Fiamma", Weight = 9.8}, cancellationToken));
74+
await (s.SaveAsync(new VerySimple {Id = 3, Name = "Roberto", Weight = 98.8 }, cancellationToken));
7475
await (tx.CommitAsync(cancellationToken));
7576
}
7677
}
@@ -86,11 +87,14 @@ public async Task OneRoundTripUpdateAsync()
8687
{
8788
var vs1 = await (s.GetAsync<VerySimple>(1));
8889
var vs2 = await (s.GetAsync<VerySimple>(2));
90+
var vs3 = await (s.GetAsync<VerySimple>(3));
8991
vs1.Weight -= 10;
9092
vs2.Weight -= 1;
93+
vs3.Weight -= 5;
9194
Sfi.Statistics.Clear();
9295
await (s.UpdateAsync(vs1));
9396
await (s.UpdateAsync(vs2));
97+
await (s.UpdateAsync(vs3));
9498
await (tx.CommitAsync());
9599
}
96100

@@ -166,9 +170,11 @@ public async Task OneRoundTripDeleteAsync()
166170
{
167171
var vs1 = await (s.GetAsync<VerySimple>(1));
168172
var vs2 = await (s.GetAsync<VerySimple>(2));
173+
var vs3 = await (s.GetAsync<VerySimple>(3));
169174
Sfi.Statistics.Clear();
170175
await (s.DeleteAsync(vs1));
171176
await (s.DeleteAsync(vs2));
177+
await (s.DeleteAsync(vs3));
172178
await (tx.CommitAsync());
173179
}
174180

0 commit comments

Comments
 (0)