【问题标题】:SaveChangesAsync works only once, then it always failsSaveChangesAsync 只工作一次,然后总是失败
【发布时间】:2018-07-28 19:44:48
【问题描述】:

我的一个查询遇到了一个奇怪的问题。当我第一次执行它时它可以工作,但所有后续调用都失败了

无法为表“消息”中的标识列插入显式值。

当我重新启动网络服务时它也可以再次工作。

我的表的架构是:

internal class Message
{
    public long Id { get; set; }
    public int TimeRangeId { get; set; }
    public byte[] Body { get; set; }
    public string BodyHash { get; set; }
    public DateTime? DeletedOn { get; set; }
    #region Navigation properties
    public TimeRange TimeRange { get; set; }
    #endregion
}

internal class Queue
{
    public int Id { get; set; }
    public string Name { get; set; }
    #region Navigation properties
    public TimeRange TimeRange { get; set; }
    #endregion
}

internal class TimeRange
{
    public int Id { get; set; }
    public int QueueId { get; set; }
    public DateTime StartsOn { get; set; }
    public DateTime EndsOn { get; set; }
    public DateTime CreatedOn { get; set; }
    #region Navigation properties
    public Queue Queue { get; set; }
    public List<Message> Messages { get; set; }
    #endregion
}

上下文设置如下:

internal class MessageQueueContext : DbContext
{
    private readonly string _connectionString;

    private readonly string _schema;

    public MessageQueueContext(string connectionString, string schema)
    {
        _connectionString = connectionString;
        _schema = schema;
    }

    public DbSet<TimeRange> TimeRanges { get; set; }

    public DbSet<Message> Messages { get; set; }

    public DbSet<Queue> Queues { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlServer(_connectionString);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {            
        modelBuilder.Entity<Message>(e =>
        {
            e.ToTable(nameof(Message), _schema).HasKey(t => t.Id);
            e.Property(p => p.Id).UseSqlServerIdentityColumn();
            e.Property(p => p.TimeRangeId).IsRequired();
            e.Property(p => p.Body).IsRequired();
            e.Property(p => p.BodyHash).IsRequired();
        });

        modelBuilder.Entity<TimeRange>(e =>
        {
            e.ToTable(nameof(TimeRange), _schema).HasKey(t => t.Id);
            e.Property(p => p.Id).UseSqlServerIdentityColumn();
            e.Property(p => p.StartsOn).IsRequired();
            e.Property(p => p.EndsOn).IsRequired();
            e.Property(p => p.CreatedOn).ValueGeneratedOnAdd();
        });

        modelBuilder.Entity<Queue>(e =>
        {
            e.ToTable(nameof(Queue), _schema).HasKey(t => t.Id);
            e.Property(p => p.Id).UseSqlServerIdentityColumn();
            e.Property(p => p.Name).IsRequired();
        });
    }
}

失败的查询是这个:

public async Task<int> EnqueueAsync(DateTime timeRangeStartsOn, DateTime timeRangeEndsOn, IEnumerable<byte[]> bodies, CancellationToken cancellationToken)
{
    const int nothingEnqueued = 0;

    using (var context = new MessageQueueContext(_connectionString, _schema))
    {
        context.Queues.Add(_queue.Value);
        context.Entry(_queue.Value).State = EntityState.Unchanged;

        var messages =
            (from body in bodies
             let bodyHash = _computeBodyHash(body).ToHexString()
             where true // CanEnqueueDuplicates || !context.Messages.AsNoTracking().Any(m => m.BodyHash == bodyHash && m.DeletedOn == null)
             select new Message
             {
                 Body = body,
                 BodyHash = bodyHash
             }).ToList();

        if (!CanEnqueueEmptyTimeRange && !messages.Any())
        {
            return nothingEnqueued;
        }

        var timeRange = new Entities.TimeRange
        {
            Queue = _queue.Value,
            StartsOn = timeRangeStartsOn,
            EndsOn = timeRangeEndsOn,
            Messages = messages
        };

        await context.TimeRanges.AddAsync(timeRange, cancellationToken);
        return await context.SaveChangesAsync(cancellationToken);
    }
}

它正确地生成了Message.Id,并且它也在数据库中正确地设置为身份


我使用SQL Server Profiler对其进行了调试,发现第二个请求与第一个请求不同:

这是第一次调用生成的:

exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [smq].[TimeRange] ([EndsOn], [QueueId], [StartsOn])
VALUES (@p0, @p1, @p2);
SELECT [Id], [CreatedOn]
FROM [smq].[TimeRange]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

',N'@p0 datetime2(7),@p1 int,@p2 datetime2(7)',@p0='2018-05-02 00:00:00',@p1=1,@p2='2018-05-01 00:00:00'


exec sp_executesql N'SET NOCOUNT ON;
DECLARE @inserted0 TABLE ([Id] bigint, [_Position] [int]);
MERGE [smq].[Message] USING (
VALUES (@p3, @p4, @p5, @p6, 0),
(@p7, @p8, @p9, @p10, 1)) AS i ([Body], [BodyHash], [DeletedOn], [TimeRangeId], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Body], [BodyHash], [DeletedOn], [TimeRangeId])
VALUES (i.[Body], i.[BodyHash], i.[DeletedOn], i.[TimeRangeId])
OUTPUT INSERTED.[Id], i._Position
INTO @inserted0;

SELECT [t].[Id] FROM [smq].[Message] t
INNER JOIN @inserted0 i ON ([t].[Id] = [i].[Id])
ORDER BY [i].[_Position];

',N'@p3 varbinary(8000),@p4 varbinary(8000),@p5 datetime2(7),@p6 int,@p7 varbinary(8000),@p8 varbinary(8000),@p9 datetime2(7),@p10 int',@p3=0x7,@p4=0x7,@p5=NULL,@p6=21,@p7=0x7,@p8=0x1,@p9=NULL,@p10=21

这是 EF 生成的所有后续调用:

exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [smq].[Message] ([Id], [Body], [BodyHash], [DeletedOn], [TimeRangeId])
VALUES (@p0, @p1, @p2, @p3, @p4),
(@p5, @p6, @p7, @p8, @p9);
INSERT INTO [smq].[TimeRange] ([EndsOn], [QueueId], [StartsOn])
VALUES (@p10, @p11, @p12);
SELECT [Id], [CreatedOn]
FROM [smq].[TimeRange]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

',N'@p0 bigint,@p1 varbinary(8000),@p2 varbinary(8000),@p3 datetime2(7),@p4 int,@p5 bigint,@p6 varbinary(8000),@p7 varbinary(8000),@p8 datetime2(7),@p9 int,@p10 datetime2(7),@p11 int,@p12 datetime2(7)',@p0=21,@p1=0x7,@p2=0x7,@p3=NULL,@p4=21,@p5=22,@p6=0x7,@p7=0x1,@p8=NULL,@p9=21,@p10='2018-05-02 00:00:00',@p11=1,@p12='2018-05-01 00:00:00'

我错过了什么显而易见的吗?调用不同看起来并不正常。他们不应该,对吗?


如果您需要任何其他信息,请告诉我,我会编辑问题。

【问题讨论】:

  • 正如您在第二个 SQL 插入中看到的那样,它试图插入一条 ID 为 21 的消息。这应该有助于您调试它。您需要找出是谁获取\创建了该消息。当您有 2 个或更多未同步的 DbContexts 时,通常会发生这种情况。一个上下文获取\创建一个跟踪的message entity,第二个上下文尝试插入一个具有消息依赖性的对象。第二个上下文不知道这条消息是由不同的上下文获取和跟踪的,并假设它是一个需要插入的新条目。
  • @AmirPopovich 感谢您查看 ;-) 这是我执行的唯一查询(通过 Postman) 看起来有某种秘密缓存存储了最近添加的消息。我认为下一个请求适用于干净的数据库上下文(不应该由using 处理吗?)。我还验证了每次调用中实体的状态是Added,直到调用SaveChangesAsync。这个不知怎么知道之前发生了什么。
  • 当我指的是查询时,我指的是服务器端 SQL 查询。插入的消息的 ID 为 21 - 以某种方式设置。首先在 Message.Id 设置器上添加断点 - stackoverflow.com/questions/4408110/…
  • @AmirPopovich 完成。 EF 首先将其设置为一些较大的负数,例如 -932342423,然后设置为新的 id,目前为 36、37。第二个请求仅设置负 id,不再调用 setter。然后它崩溃了。
  • @AmirPopovich 已修复 ;-) 如果您好奇,请查看我的回答。这是一个非常神奇的错误;-|

标签: c# entity-framework-core asp.net-core-webapi


【解决方案1】:

我找到了错误。

private readonly Lazy<Entities.Queue> _queue;

public async Task<int> EnqueueAsync(DateTime timeRangeStartsOn, DateTime timeRangeEndsOn, IEnumerable<byte[]> bodies, CancellationToken cancellationToken)
{
    const int nothingEnqueued = 0;

    using (var context = new MessageQueueContext(_connectionString, _schema))
    {
        context.Queues.Add(_queue.Value);
        context.Entry(_queue.Value).State = EntityState.Unchanged;

这是我将有关当前队列的信息添加到上下文的最后两行。由于这是一个领域,它似乎可以在上下文之间生存并以某种方式破坏它。

我把它改成了

context.Queues.Attach(new Entities.Queue 
{
    Id = _queue.Value.Id, 
    Name = _queue.Value.Name 
});

现在它可以正常工作了。简单地附加并不能修复错误(它比添加和更改状态更容易)。

真正的解决方法是创建实体的副本,因为 db-context 不喜欢它。我想人们需要了解 EF 的内部结构才能理解它......

【讨论】:

    猜你喜欢
    • 2011-05-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-14
    • 1970-01-01
    • 2022-01-22
    • 1970-01-01
    • 2012-03-05
    相关资源
    最近更新 更多