Files
Umbraco-CMS/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoFetchTests.cs
Andy Butland 12adfd52bd Performance: Reduce number of database calls in save and publish operations (#20485)
* Added request caching to media picker media retrieval, to improve performance in save operations.

* WIP: Update or insert in bulk when updating property data.

* Add tests verifying UpdateBatch.

* Fixed issue with UpdateBatch and SQL Server.

* Removed stopwatch.

* Fix test on SQLite (failing on SQLServer).

* Added temporary test for direct call to NPoco UpdateBatch.

* Fixed test on SQLServer.

* Add integration test verifying the same property data is persisted as before the performance refactor.

* Log expected warning in DocumentUrlService as debug.
2025-10-14 11:22:21 +02:00

572 lines
21 KiB
C#

// Copyright (c) Umbraco.
// See LICENSE for more details.
using NPoco;
using NUnit.Framework;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.NPocoTests;
[TestFixture]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, WithApplication = true)]
internal sealed class NPocoFetchTests : UmbracoIntegrationTest
{
[SetUp]
protected void SeedDatabase()
{
using (var scope = ScopeProvider.CreateScope())
{
InsertData(ScopeAccessor.AmbientScope.Database);
scope.Complete();
}
}
private static void InsertData(IDatabase database)
{
database.Execute(@"
CREATE TABLE zbThing1 (
id int PRIMARY KEY NOT NULL,
name NVARCHAR(255) NULL
);");
database.Insert(new Thing1Dto { Id = 1, Name = "one" });
database.Insert(new Thing1Dto { Id = 2, Name = "two" });
database.Execute(@"
CREATE TABLE zbThing2 (
id int PRIMARY KEY NOT NULL,
name NVARCHAR(255) NULL,
thingId int NULL
);");
database.Insert(new Thing2Dto { Id = 1, Name = "uno", ThingId = 1 });
database.Insert(new Thing2Dto { Id = 2, Name = "due", ThingId = 2 });
database.Insert(new Thing2Dto { Id = 3, Name = "tri", ThingId = 1 });
database.Execute(@"
CREATE TABLE zbThingGroup (
id int PRIMARY KEY NOT NULL,
name NVARCHAR(255) NULL
);");
database.Insert(new ThingGroupDto { Id = 1, Name = "g-one" });
database.Insert(new ThingGroupDto { Id = 2, Name = "g-two" });
database.Insert(new ThingGroupDto { Id = 3, Name = "g-three" });
database.Execute(@"
CREATE TABLE zbThing2Group (
thingId int NOT NULL,
groupId int NOT NULL
);");
database.Insert(new Thing2GroupDto { ThingId = 1, GroupId = 1 });
database.Insert(new Thing2GroupDto { ThingId = 1, GroupId = 2 });
database.Insert(new Thing2GroupDto { ThingId = 2, GroupId = 2 });
database.Insert(new Thing2GroupDto { ThingId = 3, GroupId = 3 });
database.Execute(@"
CREATE TABLE zbThingA1 (
id int PRIMARY KEY NOT NULL,
name NVARCHAR(255) NULL
);");
database.Execute(@"
CREATE TABLE zbThingA2 (
id int PRIMARY KEY NOT NULL,
name NVARCHAR(255) NULL
);");
database.Execute(@"
CREATE TABLE zbThingA3 (
id int PRIMARY KEY NOT NULL,
name NVARCHAR(255) NULL
);");
database.Execute(@"
CREATE TABLE zbThingA12 (
thing1id int NOT NULL,
thing2id int NOT NULL,
name NVARCHAR(255) NOT NULL
);");
}
[Test]
public void TestSimple()
{
// Fetching a simple POCO
using (var scope = ScopeProvider.CreateScope())
{
// This is the raw SQL, but it's better to use expressions and no magic strings!
// var sql = @"
// SELECT zbThing1.id, zbThing1.name
// FROM zbThing1";
var sql = ScopeAccessor.AmbientScope.SqlContext.Sql()
.Select<Thing1Dto>()
.From<Thing1Dto>();
var dtos = ScopeAccessor.AmbientScope.Database.Fetch<Thing1Dto>(sql);
Assert.AreEqual(2, dtos.Count);
Assert.AreEqual("one", dtos.First(x => x.Id == 1).Name);
}
}
[Test]
public void TestOneToOne()
{
// Fetching a POCO that contains the ID of another POCO,
// and fetching that other POCO at the same time.
using (var scope = ScopeProvider.CreateScope())
{
// This is the raw SQL, but it's better to use expressions and no magic strings!
// var sql = @"
// SELECT zbThing2.id, zbThing2.name, zbThing2.thingId,
// zbThing1.id Thing__id, zbThing1.name Thing__name
// FROM zbThing2
// JOIN zbThing1 ON zbThing2.thingId=zbThing1.id";
var sql = ScopeAccessor.AmbientScope.SqlContext.Sql()
.Select<Thing2Dto>(r => r.Select(x => x.Thing))
.From<Thing2Dto>()
.InnerJoin<Thing1Dto>().On<Thing2Dto, Thing1Dto>((t2, t1) => t2.ThingId == t1.Id);
var dtos = ScopeAccessor.AmbientScope.Database.Fetch<Thing2Dto>(sql);
Assert.AreEqual(3, dtos.Count);
Assert.AreEqual("uno", dtos.First(x => x.Id == 1).Name);
Assert.IsNotNull(dtos.First(x => x.Id == 1).Thing);
Assert.AreEqual("one", dtos.First(x => x.Id == 1).Thing.Name);
}
}
[Test]
public void TestOneToManyOnOne()
{
// Fetching a POCO that has a list of other POCOs,
// and fetching these POCOs at the same time,
// with a pk/fk relationship
// for one single POCO.
using (var scope = ScopeProvider.CreateScope())
{
// This is the raw SQL, but it's better to use expressions and no magic strings!
// var dtos = scope.Database.FetchOneToMany<Thing3Dto>(x => x.Things, x => x.Id, @"
// SELECT zbThing1.id AS Id, zbThing1.name AS Name,
// zbThing2.id AS Things__Id, zbThing2.name AS Things__Name, zbThing2.thingId AS Things__ThingId
// FROM zbThing1
// JOIN zbThing2 ON zbThing1.id=zbThing2.thingId
// WHERE zbThing1.id=1");
var sql = ScopeAccessor.AmbientScope.SqlContext.Sql()
.Select<Thing3Dto>(r => r.Select(x => x.Things))
.From<Thing3Dto>()
.InnerJoin<Thing2Dto>().On<Thing3Dto, Thing2Dto>(left => left.Id, right => right.ThingId)
.Where<Thing3Dto>(x => x.Id == 1);
// var dtos = scope.Database.FetchOneToMany<Thing3Dto>(x => x.Things, x => x.Id, sql);
var dtos = ScopeAccessor.AmbientScope.Database.FetchOneToMany<Thing3Dto>(x => x.Things, sql);
Assert.AreEqual(1, dtos.Count);
var dto1 = dtos.FirstOrDefault(x => x.Id == 1);
Assert.IsNotNull(dto1);
Assert.AreEqual("one", dto1.Name);
Assert.IsNotNull(dto1.Things);
Assert.AreEqual(2, dto1.Things.Count);
var dto2 = dto1.Things.FirstOrDefault(x => x.Id == 1);
Assert.IsNotNull(dto2);
Assert.AreEqual("uno", dto2.Name);
}
}
[Test]
public void TestOneToManyOnMany()
{
// Fetching a POCO that has a list of other POCOs,
// and fetching these POCOs at the same time,
// with a pk/fk relationship
// for several POCOs.
//
// The ORDER BY clause (matching x => x.Id) is required
// for proper aggregation to take place.
using (var scope = ScopeProvider.CreateScope())
{
// This is the raw SQL, but it's better to use expressions and no magic strings!
// var sql = @"
// SELECT zbThing1.id AS Id, zbThing1.name AS Name,
// zbThing2.id AS Things__Id, zbThing2.name AS Things__Name, zbThing2.thingId AS Things__ThingId
// FROM zbThing1
// JOIN zbThing2 ON zbThing1.id=zbThing2.thingId
// ORDER BY zbThing1.id";
var sql = ScopeAccessor.AmbientScope.SqlContext.Sql()
.Select<Thing3Dto>(r => r.Select(x => x.Things)) // select Thing3Dto, and Thing2Dto for Things
.From<Thing3Dto>()
.InnerJoin<Thing2Dto>().On<Thing3Dto, Thing2Dto>(left => left.Id, right => right.ThingId)
.OrderBy<Thing3Dto>(x => x.Id);
var dtos = ScopeAccessor.AmbientScope.Database.FetchOneToMany<Thing3Dto>(x => x.Things, /*x => x.Id,*/ sql);
Assert.AreEqual(2, dtos.Count);
var dto1 = dtos.FirstOrDefault(x => x.Id == 1);
Assert.IsNotNull(dto1);
Assert.AreEqual("one", dto1.Name);
Assert.IsNotNull(dto1.Things);
Assert.AreEqual(2, dto1.Things.Count);
var dto2 = dto1.Things.FirstOrDefault(x => x.Id == 1);
Assert.IsNotNull(dto2);
Assert.AreEqual("uno", dto2.Name);
}
}
[Test]
public void TestOneToManyOnManyTemplate()
{
using (var scope = ScopeProvider.CreateScope())
{
ScopeAccessor.AmbientScope.SqlContext.Templates.Clear();
var sql = ScopeAccessor.AmbientScope.SqlContext.Templates.Get("xxx", s => s
.Select<Thing3Dto>(r => r.Select(x => x.Things)) // select Thing3Dto, and Thing2Dto for Things
.From<Thing3Dto>()
.InnerJoin<Thing2Dto>().On<Thing3Dto, Thing2Dto>(left => left.Id, right => right.ThingId)
.OrderBy<Thing3Dto>(x => x.Id)).Sql();
// cached
sql = ScopeAccessor.AmbientScope.SqlContext.Templates.Get("xxx", s => throw new InvalidOperationException())
.Sql();
var dtos = ScopeAccessor.AmbientScope.Database.FetchOneToMany<Thing3Dto>(x => x.Things, /*x => x.Id,*/ sql);
Assert.AreEqual(2, dtos.Count);
var dto1 = dtos.FirstOrDefault(x => x.Id == 1);
Assert.IsNotNull(dto1);
Assert.AreEqual("one", dto1.Name);
Assert.IsNotNull(dto1.Things);
Assert.AreEqual(2, dto1.Things.Count);
var dto2 = dto1.Things.FirstOrDefault(x => x.Id == 1);
Assert.IsNotNull(dto2);
Assert.AreEqual("uno", dto2.Name);
}
}
[Test]
public void TestManyToMany()
{
// Fetching a POCO that has a list of other POCOs,
// and fetching these POCOs at the same time,
// with an n-to-n intermediate table.
//
// The ORDER BY clause (matching x => x.Id) is required
// for proper aggregation to take place.
using (var scope = ScopeProvider.CreateScope())
{
// This is the raw SQL, but it's better to use expressions and no magic strings!
// var sql = @"
// SELECT zbThing1.id, zbThing1.name, zbThingGroup.id, zbThingGroup.name
// FROM zbThing1
// JOIN zbThing2Group ON zbThing1.id=zbThing2Group.thingId
// JOIN zbThingGroup ON zbThing2Group.groupId=zbThingGroup.id
// ORDER BY zbThing1.id";
var sql = ScopeAccessor.AmbientScope.SqlContext.Sql()
.Select<Thing4Dto>(r => r.Select(x => x.Groups))
.From<Thing4Dto>()
.InnerJoin<Thing2GroupDto>().On<Thing4Dto, Thing2GroupDto>((t, t2g) => t.Id == t2g.ThingId)
.InnerJoin<ThingGroupDto>().On<Thing2GroupDto, ThingGroupDto>((t2g, tg) => t2g.GroupId == tg.Id)
.OrderBy<Thing4Dto>(x => x.Id);
var dtos = ScopeAccessor.AmbientScope.Database.FetchOneToMany<Thing4Dto>(x => x.Groups, /*x => x.Id,*/ sql);
Assert.AreEqual(2, dtos.Count);
var dto1 = dtos.FirstOrDefault(x => x.Id == 1);
Assert.IsNotNull(dto1);
Assert.AreEqual("one", dto1.Name);
Assert.IsNotNull(dto1.Groups);
Assert.AreEqual(2, dto1.Groups.Count);
var dto2 = dto1.Groups.FirstOrDefault(x => x.Id == 1);
Assert.IsNotNull(dto2);
Assert.AreEqual("g-one", dto2.Name);
}
}
[Test]
public void TestCalculated()
{
// Fetching a POCO that has a countof other POCOs,
// with an n-to-n intermediate table.
using (var scope = ScopeProvider.CreateScope())
{
// This is the raw SQL, but it's better to use expressions and no magic strings!
// var sql = @"
// SELECT zbThing1.id, zbThing1.name, COUNT(zbThing2Group.groupId) as groupCount
// FROM zbThing1
// JOIN zbThing2Group ON zbThing1.id=zbThing2Group.thingId
// GROUP BY zbThing1.id, zbThing1.name";
var sql = ScopeAccessor.AmbientScope.SqlContext.Sql()
.Select<Thing1Dto>()
.Append(", COUNT(zbThing2Group.groupId) AS groupCount")
.From<Thing1Dto>()
.InnerJoin<Thing2GroupDto>().On<Thing1Dto, Thing2GroupDto>((t, t2g) => t.Id == t2g.ThingId)
.GroupBy<Thing1Dto>(x => x.Id, x => x.Name);
var dtos = ScopeAccessor.AmbientScope.Database.Fetch<Thing5Dto>(sql);
Assert.AreEqual(2, dtos.Count);
var dto1 = dtos.FirstOrDefault(x => x.Id == 1);
Assert.IsNotNull(dto1);
Assert.AreEqual("one", dto1.Name);
Assert.AreEqual(2, dto1.GroupCount);
var dto2 = dtos.FirstOrDefault(x => x.Id == 2);
Assert.IsNotNull(dto2);
Assert.AreEqual("two", dto2.Name);
Assert.AreEqual(1, dto2.GroupCount);
}
}
// no test for ReferenceType.Foreign at the moment
// it's more or less OneToOne, but NPoco manages the keys when
// inserting or updating
[Test]
public void TestSql()
{
using (var scope = ScopeProvider.CreateScope())
{
var sql = ScopeAccessor.AmbientScope.SqlContext.Sql()
.SelectAll()
.From<Thing1Dto>()
.Where<Thing1Dto>(x => x.Id == 1);
var dto = ScopeAccessor.AmbientScope.Database.Fetch<Thing1Dto>(sql).FirstOrDefault();
Assert.IsNotNull(dto);
Assert.AreEqual("one", dto.Name);
//// var sql2 = new Sql(sql.SQL, new { id = 1 });
//// WriteSql(sql2);
//// dto = Database.Fetch<Thing1Dto>(sql2).FirstOrDefault();
//// Assert.IsNotNull(dto);
//// Assert.AreEqual("one", dto.Name);
var sql3 = new Sql(sql.SQL, 1);
dto = ScopeAccessor.AmbientScope.Database.Fetch<Thing1Dto>(sql3).FirstOrDefault();
Assert.IsNotNull(dto);
Assert.AreEqual("one", dto.Name);
}
}
[Test]
public void TestMultipleOneToOne()
{
using (var scope = ScopeProvider.CreateScope())
{
var tA1A = new ThingA1Dto { Id = 1, Name = "a1_a" };
ScopeAccessor.AmbientScope.Database.Insert(tA1A);
var tA1B = new ThingA1Dto { Id = 2, Name = "a1_b" };
ScopeAccessor.AmbientScope.Database.Insert(tA1B);
var tA1C = new ThingA1Dto { Id = 3, Name = "a1_c" };
ScopeAccessor.AmbientScope.Database.Insert(tA1C);
var tA2A = new ThingA2Dto { Id = 1, Name = "a2_a" };
ScopeAccessor.AmbientScope.Database.Insert(tA2A);
var tA2B = new ThingA2Dto { Id = 2, Name = "a2_b" };
ScopeAccessor.AmbientScope.Database.Insert(tA2B);
var tA2C = new ThingA2Dto { Id = 3, Name = "a2_c" };
ScopeAccessor.AmbientScope.Database.Insert(tA2C);
var tA3A = new ThingA3Dto { Id = 1, Name = "a3_a" };
ScopeAccessor.AmbientScope.Database.Insert(tA3A);
var tA3B = new ThingA3Dto { Id = 2, Name = "a3_b" };
ScopeAccessor.AmbientScope.Database.Insert(tA3B);
var k1 = new ThingA12Dto { Name = "a", Thing1Id = tA1A.Id, Thing2Id = tA2A.Id };
ScopeAccessor.AmbientScope.Database.Insert(k1);
var k2 = new ThingA12Dto { Name = "b", Thing1Id = tA1A.Id, Thing2Id = tA2B.Id };
ScopeAccessor.AmbientScope.Database.Insert(k2);
var sql = @"SELECT a1.id, a1.name,
a2.id AS T2A__Id, a2.name AS T2A__Name, a3.id AS T2A__T3__Id, a3.name AS T2A__T3__Name,
a2x.id AS T2B__Id, a2x.name AS T2B__Name, a3x.id AS T2B__T3__Id, a3x.name AS T2B__T3__Name
FROM zbThingA1 a1
JOIN zbThingA12 a12 ON a1.id=a12.thing1id AND a12.name='a'
JOIN zbThingA2 a2 ON a12.thing2id=a2.id
JOIN zbThingA3 a3 ON a2.id=a3.id
JOIN zbThingA12 a12x ON a1.id=a12x.thing1id AND a12x.name='b'
JOIN zbThingA2 a2x ON a12x.thing2id=a2x.id
JOIN zbThingA3 a3x ON a2x.id=a3x.id
";
var ts = ScopeAccessor.AmbientScope.Database.Fetch<ThingA1Dto>(sql);
Assert.AreEqual(1, ts.Count);
var t = ts.First();
Assert.AreEqual("a1_a", t.Name);
Assert.AreEqual("a2_a", t.T2A.Name);
Assert.AreEqual("a2_b", t.T2B.Name);
Assert.AreEqual("a3_a", t.T2A.T3.Name);
Assert.AreEqual("a3_b", t.T2B.T3.Name);
scope.Complete();
}
}
[TableName("zbThing1")]
[PrimaryKey("id", AutoIncrement = false)]
[ExplicitColumns]
public class Thing1Dto
{
[Column("id")]
public int Id { get; set; }
[Column("name")]
public string Name { get; set; }
}
[TableName("zbThing2")]
[PrimaryKey("id", AutoIncrement = false)]
[ExplicitColumns]
public class Thing2Dto
{
[Column("id")]
public int Id { get; set; }
[Column("name")]
public string Name { get; set; }
[Column("thingId")]
public int ThingId { get; set; }
// reference is required else value remains null
// columnName indicates which column has the id, referenceMembreName not needed if PK
[Reference(ReferenceType.OneToOne, ColumnName = "thingId" /*, ReferenceMemberName="id"*/)]
public Thing1Dto Thing { get; set; }
}
[TableName("zbThing1")]
[PrimaryKey("id", AutoIncrement = false)]
[ExplicitColumns]
public class Thing3Dto
{
[Column("id")]
public int Id { get; set; }
[Column("name")]
public string Name { get; set; }
// reference is required else FetchOneToMany aggregation does not happen
// does not seem to require ReferenceMemberName="thingId", ColumnName not needed if PK
[Reference(ReferenceType.Many /*, ColumnName="id", ReferenceMemberName="thingId"*/)]
public List<Thing2Dto> Things { get; set; }
}
[TableName("zbThingGroup")]
[PrimaryKey("id", AutoIncrement = false)]
[ExplicitColumns]
public class ThingGroupDto
{
[Column("id")]
public int Id { get; set; }
[Column("name")]
public string Name { get; set; }
}
[TableName("zbThing2Group")]
[PrimaryKey("thingId, groupId", AutoIncrement = false)]
[ExplicitColumns]
public class Thing2GroupDto
{
[Column("thingId")]
public int ThingId { get; set; }
[Column("groupId")]
public int GroupId { get; set; }
}
[TableName("zbThing1")]
[PrimaryKey("id", AutoIncrement = false)]
[ExplicitColumns]
public class Thing4Dto
{
[Column("id")] public int Id { get; set; }
[Column("name")] public string Name { get; set; }
// reference is required else FetchOneToMany aggregation does not happen
// not sure ColumnName nor ReferenceMemberName make much sense here
[Reference(ReferenceType.Many /*, ColumnName="id", ReferenceMemberName="thingId"*/)]
public List<ThingGroupDto> Groups { get; set; }
}
[TableName("zbThing1")]
[PrimaryKey("id", AutoIncrement = false)]
[ExplicitColumns]
public class Thing5Dto
{
[Column("id")] public int Id { get; set; }
[Column("name")] public string Name { get; set; }
[Column("groupCount")]
[ResultColumn] // not included in insert/update, not sql-generated
public int GroupCount { get; set; }
}
[TableName("zbThingA1")]
[PrimaryKey("id", AutoIncrement = false)]
[ExplicitColumns]
public class ThingA1Dto
{
[Column("id")]
public int Id { get; set; }
[Column("name")]
public string Name { get; set; }
[ResultColumn]
[Reference(ReferenceType.OneToOne)]
public ThingA2Dto T2A { get; set; }
[ResultColumn]
[Reference(ReferenceType.OneToOne)]
public ThingA2Dto T2B { get; set; }
}
[TableName("zbThingA2")]
[PrimaryKey("id", AutoIncrement = false)]
[ExplicitColumns]
public class ThingA2Dto
{
[Column("id")] public int Id { get; set; }
[Column("name")] public string Name { get; set; }
[ResultColumn]
[Reference(ReferenceType.OneToOne)]
public ThingA3Dto T3 { get; set; }
}
[TableName("zbThingA3")]
[PrimaryKey("id", AutoIncrement = false)]
[ExplicitColumns]
public class ThingA3Dto
{
[Column("id")] public int Id { get; set; }
[Column("name")] public string Name { get; set; }
}
[TableName("zbThingA12")]
[ExplicitColumns]
public class ThingA12Dto
{
[Column("thing1id")] public int Thing1Id { get; set; }
[Column("thing2id")] public int Thing2Id { get; set; }
[Column("name")] public string Name { get; set; }
}
}