首页 > 解决方案 > 无法使用实体框架删除行

问题描述

尝试使用 Entity Framework 删除行时遇到问题。它说我遇到了并发问题,但我不明白为什么会这样。

using (AppHost_DataHubEntities db = new AppHost_DataHubEntities())
{
    DateTime tooOld = (DateTime.Now - HISTORY_TIME_SPAN);
    var asd = db.FromDataHub.RemoveRange(db.FromDataHub.Where(x => x.DateTime < tooOld));
    db.SaveChanges();
}

在这里,SaveChanges()抛出以下错误:

存储更新、插入或删除语句影响了意外数量的行 (0)。自加载实体后,实体可能已被修改或删除。有关了解和处理选项的信息,请参阅http: //go.microsoft.com/fwlink/ ?LinkId=472540...

据我了解,这意味着我试图删除的行已被修改。但是,如果这是我机器上只有这个应用程序可以访问的开发数据库,​​这怎么可能呢?

我还查看了行的状态。并在通话后正确更改为“已删除” RemoveRange()

对此有什么想法吗?

如果需要,很乐意提供更多信息。

编辑:

DbContext 定义:

public partial class AppHost_DataHubEntities : DbContext
{
    public AppHost_DataHubEntities()
        : base("name=AppHost_DataHubEntities")
    {
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        throw new UnintentionalCodeFirstException();
    }

    public virtual DbSet<FromDataHub> FromDataHub { get; set; }
    public virtual DbSet<Point> Point { get; set; }
    public virtual DbSet<ToDataHub> ToDataHub { get; set; }
}

ERD 的屏幕截图:

ERD

重现项目:

档案网

标签: c#entity-frameworkconcurrency

解决方案


有几件事要尝试,评论太多了:

尝试删除单个“旧”项:

using (AppHost_DataHubEntities db = new AppHost_DataHubEntities())
{
    DateTime tooOld = (DateTime.Now - HISTORY_TIME_SPAN);
    var item = db.FromDataHub.Where(x => x.DateTime < tooOld).First();
    db.FromDataHub.Remove(item);
    db.SaveChanges();
}

它会删除预期的行吗?它似乎改变了相关点中的任何内容吗?

接下来,尝试一个范围:

using (AppHost_DataHubEntities db = new AppHost_DataHubEntities())
{
    DateTime tooOld = (DateTime.Now - HISTORY_TIME_SPAN);
    var items = db.FromDataHub.Where(x => x.DateTime < tooOld).Take(3).ToList();
    db.FromDataHub.RemoveRange(items);
    db.SaveChanges();
}

同样检查任何错误或意外修改?这些“点”记录中的每一个都涉及到哪些“点”记录?同一个?不同的?

接下来,检查数据是否有可能删除所有From 记录,一个点的所有项目早于阈值,获取该点 ID:

using (AppHost_DataHubEntities db = new AppHost_DataHubEntities())
{
    DateTime tooOld = (DateTime.Now - HISTORY_TIME_SPAN);
    var items = db.FromDataHub.Where(x => x.DateTime < tooOld && x.PointId == pointId)..ToList();
    db.FromDataHub.RemoveRange(items);
    db.SaveChanges();
}

这行得通吗?

基本上,当遇到此类问题时,您需要计算出最小公分母,以查看是否存在映射问题或在所有情况下都会导致错误,或者在数据可能存在的情况下导致错误的特定数据组合确定删除。

如果您无法删除单行,则映射中的某些内容正在阻止它。如果您可以删除单行,但某个范围会导致问题,无论是跨单个点还是不同点,则需要进一步钻探以确定原因。如果这三个都有效,您可以尝试在呼叫中添加.ToList()内部。DeleteRange

通常,我不会对 EF 进行批量删除以进行维护,而是设置一个计划的作业以在数小时后直接针对数据库运行。如果我确实想在 EF 中进行批量删除,我通常会将其分解为工作块。例如:

List<FromDataHub> itemsToClean = new List<FromDataHub>();
using(var context = new AppHost_DataHubEntities())
{
    itemsToClean = context.FromDataHub
        .Where(x => x.DateTime < tooOld)
        .Select(x => new FromDataHub { PointId = x.PointId, DateTime = x.DateTime })
        .ToList();
}

const int batchSize = 500;
int batchCount = 1;
var batch = itemsToClean.Take(batchSize);
while(batch.Any())
{
    using(var context = new AppHost_DataHubEntities())
    {
        foreach(var item in batch)
        {
            context.FromDataHub.Attach(item);
            context.Entity(item).EntityState = EntityState.Deleted;
        }
        context.SaveChanges();
    }
    batch = itemsToClean.Skip(++batchCount).Take(batchSize);
}

该代码基本上不在脑海中,而且已经很晚了,但应该给出这个想法。对于潜在的大型实体,我会考虑特定于此目的的有界上下文,其中为该上下文注册的已定义“FromDataHub”实体仅包含 ID 列。(在这种情况下,您可以将其简化为批量读取和删除。此示例所做的是执行查询以仅返回相关的 ID 列(假设为 PointId 和 DateTime)并从中填充分离的实体。然后分批500 它打开一个新的 DbContext,附加该实体,将其修改状态设置为已删除,然后保存批处理。这是为了防止在 DbContext 实例中跟踪大量实体,以防此代码需要删除大量行。再次,

更新:您将需要更新您的示例以提供完整且最小的可重现示例。我已经根据您提供的内容设置了一个架构,但无法使用 EF 重现该问题:

实体:

[Table("Points")]
public class Point
{
    [Key]
    public int PointId { get; set; }
    public string PointName { get; set; }
    public bool Collect { get; set; }

    public virtual ICollection<FromDataHub> FromDataHubs { get; set; } = new List<FromDataHub>();
    public virtual ICollection<ToDataHub> ToDataHubs { get; set; } = new List<ToDataHub>();
}

[Table("FromDataHub")]
public class FromDataHub
{
    [Key, Column(Order = 0), ForeignKey("Point")]
    public int PointId { get; set; }
    [Key, Column(Order = 1)]
    public DateTime DateTime { get; set; }
    [Key, Column(Order = 2)]
    public int Value { get; set; }

    public virtual Point Point { get; set; }
}

[Table("ToDataHub")]
public class ToDataHub
{
    [Key, Column(Order = 0), ForeignKey("Point")]
    public int PointId { get; set; }
    [Key, Column(Order = 1)]
    public DateTime DateTime { get; set; }

    public virtual Point Point { get; set; }
}

带映射:

modelBuilder.Entity<Point>()
    .HasMany(x => x.FromDataHubs)
    .WithRequired(x => x.Point);

modelBuilder.Entity<Point>()
    .HasMany(x => x.ToDataHubs)
    .WithRequired(x => x.Point); 

...这是完全可选的,它在没有显式映射的情况下工作。

考试:

using (var context = new TestDbContext())
{
    DateTime date = DateTime.Today.AddMonths(-6);
    var itemsToDelete = context.FromDataHubs.Where(x => x.DateTime < date);
    context.FromDataHubs.RemoveRange(itemsToDelete);
    context.SaveChanges();
}

在用几个 Points 和少量 From 和 To DataHub 记录(范围小于和大于 6 个月)为表播种后,运行此代码成功删除了旧记录,没有任何异常。

如果您的类与上述有显着差异,请发布您当前使用的实体定义和任何映射配置。

更新#2:

好吧,哇,这是一个谜。我能够发现的是,由于某种原因,这个问题似乎是您的数据库所特有的。我修改了我的架构以匹配您的架构,并修改了您的数据库以消除可能的怪异。我还将您的测试数据复制到我的数据库中,以防它是与数据相关的问题。结果是我对您的数据库的测试继续失败并出现相同的异常,而您的应用程序针对我的数据库成功。

您的架构:

USE [StackExample]
GO
/****** Object:  Table [dbo].[FromDataHub]    Script Date: 25/11/2020 8:50:04 AM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[FromDataHub](
    [PointId] [int] NOT NULL,
    [DateTime] [datetime] NOT NULL,
    [Value] [float] NOT NULL,
 CONSTRAINT [PK_FromDataHub] PRIMARY KEY CLUSTERED 
(
    [PointId] ASC,
    [DateTime] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object:  Table [dbo].[Point]    Script Date: 25/11/2020 8:50:04 AM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Point](
    [PointId] [int] IDENTITY(1,1) NOT NULL,
    [PointName] [varchar](255) NOT NULL,
    [Collect] [bit] NOT NULL,
 CONSTRAINT [PK_Point] PRIMARY KEY CLUSTERED 
(
    [PointId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object:  Table [dbo].[ToDataHub]    Script Date: 25/11/2020 8:50:04 AM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[ToDataHub](
    [PointId] [int] NOT NULL,
    [Value] [float] NOT NULL,
 CONSTRAINT [PK_ToDataHub] PRIMARY KEY CLUSTERED 
(
    [PointId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[FromDataHub]  WITH CHECK ADD  CONSTRAINT [FK_FromDataHub_Point] FOREIGN KEY([PointId])
REFERENCES [dbo].[Point] ([PointId])
GO
ALTER TABLE [dbo].[FromDataHub] CHECK CONSTRAINT [FK_FromDataHub_Point]
GO
ALTER TABLE [dbo].[ToDataHub]  WITH CHECK ADD  CONSTRAINT [FK_ToDataHub_Point] FOREIGN KEY([PointId])
REFERENCES [dbo].[Point] ([PointId])
GO
ALTER TABLE [dbo].[ToDataHub] CHECK CONSTRAINT [FK_ToDataHub_Point]
GO

我的架构

USE [Spikes]
GO
/****** Object:  Table [dbo].[FromDataHub]    Script Date: 25/11/2020 8:50:44 AM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[FromDataHub](
    [PointId] [int] NOT NULL,
    [DateTime] [datetime] NOT NULL,
    [Value] [float] NOT NULL,
 CONSTRAINT [PK_FromDataHub_1] PRIMARY KEY CLUSTERED 
(
    [PointId] ASC,
    [DateTime] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object:  Table [dbo].[Point]    Script Date: 25/11/2020 8:50:44 AM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Point](
    [PointId] [int] NOT NULL,
    [PointName] [nvarchar](20) NOT NULL,
    [Collect] [bit] NOT NULL,
 CONSTRAINT [PK_Points] PRIMARY KEY CLUSTERED 
(
    [PointId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object:  Table [dbo].[ToDataHub]    Script Date: 25/11/2020 8:50:44 AM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[ToDataHub](
    [PointId] [int] NOT NULL,
    [Value] [float] NOT NULL,
 CONSTRAINT [PK_ToDataHub_1] PRIMARY KEY CLUSTERED 
(
    [PointId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[Point] ADD  CONSTRAINT [DF_Points_Collect]  DEFAULT ((1)) FOR [Collect]
GO
ALTER TABLE [dbo].[FromDataHub]  WITH CHECK ADD  CONSTRAINT [FK_FromDataHub_Points] FOREIGN KEY([PointId])
REFERENCES [dbo].[Point] ([PointId])
GO
ALTER TABLE [dbo].[FromDataHub] CHECK CONSTRAINT [FK_FromDataHub_Points]
GO
ALTER TABLE [dbo].[ToDataHub]  WITH CHECK ADD  CONSTRAINT 
[FK_ToDataHub_Point] FOREIGN KEY([PointId])
REFERENCES [dbo].[Point] ([PointId])
GO
ALTER TABLE [dbo].[ToDataHub] CHECK CONSTRAINT [FK_ToDataHub_Point]
GO

两者的数据是相同的。我没有更改的唯一架构差异是 PointName 的数据类型/大小。我对 Collect 值也有默认约束。在这两种情况下,原始模式的更改基本上是将点 ID PK 和 FK 从“ID”和“Point”更改为“PointId”。最初我的架构没有声明 FK。

我要注意的一件事是我无法让您的 EDMX 开箱即用。我注意到它声明的实体缺少实体的任何 Key 或 FK 声明。我不得不手动添加这些并禁用 CodeFirst 断言:

public AppHost_DataHubEntities : base("name=AppHost_DataHubEntities")
{
    Database.SetInitializer<AppHost_DataHubEntities>(null);
}

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    //throw new UnintentionalCodeFirstException();
    modelBuilder.Entity<Point>()
        .ToTable("Point")
        .HasKey(x => x.PointId)
        .HasMany(x => x.FromDataHub)            
        .WithRequired(x => x.Point)
        .HasForeignKey(x => x.PointId);
    //modelBuilder.Entity<Point>()
    //    .HasKey(x => x.ID)
    //    .HasOptional(x => x.ToDataHub)
    //    .WithRequired(x => x.Point1);

    modelBuilder.Entity<FromDataHub>()
        .ToTable("FromDataHub")
        .HasKey(x => new { x.PointId, x.DateTime });

    modelBuilder.Entity<ToDataHub>()
        .ToTable("ToDataHub")
        .HasKey(x => new { x.PointId });

    }

测试代码:

using (var context = new SoDbContext())
{
    DateTime tooOld = DateTime.Now - new TimeSpan(0, 15, 0);
    var items = context.FromDataHubs.Where(x => x.DateTime < tooOld).ToList();
    //db.FromDataHub.RemoveRange(items);
    context.FromDataHubs.Remove(items.First());
    context.SaveChanges();
}

我还将 DbContext 上的初始化程序设置为 null 并删除了迁移,但错误仍然存​​在于您的测试数据库中。因此,如果我将您的应用程序配置指向您的数据库,我会收到错误消息。如果我将它指向我的“Spikes”数据库,那么它可以毫无问题地运行。我之前的单元测试也是如此。

关于我唯一可以建议的是避免 EDMX & Code First 生成并手动创建所需的模式,将 EF 实体映射到它。生成的模式中似乎有一些微妙的东西,奇怪的是在为表或数据库级别生成的 SQL 中并不明显?


推荐阅读