// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.Infrastructure; using EntityFrameworkCore.Jet.Diagnostics.Internal; using EntityFrameworkCore.Jet.FunctionalTests.TestUtilities; using EntityFrameworkCore.Jet.Infrastructure; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; // ReSharper disable UnusedAutoPropertyAccessor.Local // ReSharper disable InconsistentNaming namespace EntityFrameworkCore.Jet.FunctionalTests { public class BatchingTest : IClassFixture { public BatchingTest(BatchingTestFixture fixture) { Fixture = fixture; } protected BatchingTestFixture Fixture { get; } [ConditionalTheory] [InlineData(true, true, true)] [InlineData(false, true, true)] [InlineData(true, false, true)] [InlineData(false, false, true)] [InlineData(true, true, false)] [InlineData(false, true, false)] [InlineData(true, false, false)] [InlineData(false, false, false)] public void Inserts_are_batched_correctly(bool clientPk, bool clientFk, bool clientOrder) { var expectedBlogs = new List(); ExecuteWithStrategyInTransaction( context => { var owner1 = new Owner(); var owner2 = new Owner(); context.Owners.Add(owner1); context.Owners.Add(owner2); for (var i = 1; i < 500; i++) { var blog = new Blog(); if (clientPk) { blog.Id = Guid.NewGuid(); } if (clientFk) { blog.Owner = i % 2 == 0 ? owner1 : owner2; } if (clientOrder) { blog.Order = i; } context.Set().Add(blog); expectedBlogs.Add(blog); } context.SaveChanges(); }, context => AssertDatabaseState(context, clientOrder, expectedBlogs)); } [ConditionalFact] public void Inserts_and_updates_are_batched_correctly() { var expectedBlogs = new List(); ExecuteWithStrategyInTransaction( context => { var owner1 = new Owner { Name = "0" }; var owner2 = new Owner { Name = "1" }; context.Owners.Add(owner1); context.Owners.Add(owner2); var blog1 = new Blog { Id = Guid.NewGuid(), Owner = owner1, Order = 1 }; context.Set().Add(blog1); expectedBlogs.Add(blog1); context.SaveChanges(); owner2.Name = "2"; blog1.Order = 0; var blog2 = new Blog { Id = Guid.NewGuid(), Owner = owner1, Order = 1 }; context.Set().Add(blog2); expectedBlogs.Add(blog2); var blog3 = new Blog { Id = Guid.NewGuid(), Owner = owner2, Order = 2 }; context.Set().Add(blog3); expectedBlogs.Add(blog3); context.SaveChanges(); }, context => AssertDatabaseState(context, true, expectedBlogs)); } [ConditionalFact] public void Inserts_when_database_type_is_different() { ExecuteWithStrategyInTransaction( context => { var owner1 = new Owner { Id = "0", Name = "Zero" }; var owner2 = new Owner { Id = "A", Name = string.Join("", Enumerable.Repeat('A', 255)) }; context.Owners.Add(owner1); context.Owners.Add(owner2); context.SaveChanges(); }, context => Assert.Equal(2, context.Owners.Count())); } [ConditionalTheory] [InlineData(3)] [InlineData(4)] public void Inserts_are_batched_only_when_necessary(int minBatchSize) { var expectedBlogs = new List(); TestHelpers.ExecuteWithStrategyInTransaction( () => (BloggingContext)Fixture.CreateContext(minBatchSize), UseTransaction, context => { var owner = new Owner(); context.Owners.Add(owner); for (var i = 1; i < 4; i++) { var blog = new Blog { Id = Guid.NewGuid(), Owner = owner }; context.Set().Add(blog); expectedBlogs.Add(blog); } Fixture.TestSqlLoggerFactory.Clear(); context.SaveChanges(); Assert.Contains( minBatchSize == 3 ? RelationalResources.LogBatchReadyForExecution(new TestLogger()) .GenerateMessage(3) : RelationalResources.LogBatchSmallerThanMinBatchSize(new TestLogger()) .GenerateMessage(3, 4), Fixture.TestSqlLoggerFactory.Log.Select(l => l.Message)); Assert.Equal(minBatchSize <= 3 ? 2 : 4, Fixture.TestSqlLoggerFactory.SqlStatements.Count); }, context => AssertDatabaseState(context, false, expectedBlogs)); } private void AssertDatabaseState(DbContext context, bool clientOrder, List expectedBlogs) { expectedBlogs = clientOrder ? expectedBlogs.OrderBy(b => b.Order).ToList() : expectedBlogs.OrderBy(b => b.Id).ToList(); var actualBlogs = clientOrder ? context.Set().OrderBy(b => b.Order).ToList() : expectedBlogs.OrderBy(b => b.Id).ToList(); Assert.Equal(expectedBlogs.Count, actualBlogs.Count); for (var i = 0; i < actualBlogs.Count; i++) { var expected = expectedBlogs[i]; var actual = actualBlogs[i]; Assert.Equal(expected.Id, actual.Id); Assert.Equal(expected.Order, actual.Order); Assert.Equal(expected.OwnerId, actual.OwnerId); Assert.Equal(expected.Version, actual.Version); } } private BloggingContext CreateContext() => (BloggingContext)Fixture.CreateContext(); private void ExecuteWithStrategyInTransaction( Action testOperation, Action nestedTestOperation) => TestHelpers.ExecuteWithStrategyInTransaction( CreateContext, UseTransaction, testOperation, nestedTestOperation); protected void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) => facade.UseTransaction(transaction.GetDbTransaction()); private class BloggingContext : PoolableDbContext { public BloggingContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity( b => { b.Property(e => e.Id).ValueGeneratedOnAdd(); b.Property(e => e.Version).IsConcurrencyToken().ValueGeneratedOnAddOrUpdate(); b.Property(e => e.Name).HasColumnType("nvarchar(255)"); }); modelBuilder.Entity( b => { b.Property(e => e.Id).HasDefaultValueSql("GenGUID()"); b.Property(e => e.Version).IsConcurrencyToken().ValueGeneratedOnAddOrUpdate(); }); } // ReSharper disable once UnusedMember.Local public DbSet Blogs { get; set; } public DbSet Owners { get; set; } } private class Blog { public Guid Id { get; set; } public int Order { get; set; } public string OwnerId { get; set; } public Owner Owner { get; set; } public byte[] Version { get; set; } } private class Owner { public string Id { get; set; } public string Name { get; set; } public byte[] Version { get; set; } } public class BatchingTestFixture : SharedStoreFixtureBase { protected override string StoreName { get; } = "BatchingTest"; public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; protected override ITestStoreFactory TestStoreFactory => JetTestStoreFactory.Instance; protected override Type ContextType { get; } = typeof(BloggingContext); protected override bool ShouldLogCategory(string logCategory) => logCategory == DbLoggerCategory.Update.Name; protected override void Seed(PoolableDbContext context) { context.Database.EnsureCreatedResiliently(); context.Database.ExecuteSqlRaw( @" ALTER TABLE Owners ALTER COLUMN Name nvarchar(255);"); } public DbContext CreateContext(int minBatchSize) { var optionsBuilder = new DbContextOptionsBuilder(CreateOptions()); new JetDbContextOptionsBuilder(optionsBuilder).MinBatchSize(minBatchSize); return new BloggingContext(optionsBuilder.Options); } } } }