实体框架插入与另一个外键冲突

时间:2019-06-13 15:36:05

标签: sql .net entity-framework entity-framework-core asp.net-core-2.0

我很难插入带有其相关实体的数据。

public class Status : Entity, IAggregateRoot
 {
    //other properties
     public readonly List<Video> _videos;
     public readonly List<Photo> _photos;
 }

-

public class Log : Entity, IAggregateRoot
  {
    //other properties
     public readonly List<Video> _videos;
     public readonly List<Photo> _photos;
     public readonly List<Sound> _audios;
  }

-

public class Photo : Entity, IAggregateRoot
    {
        //other properties
        public string Type { get; set; }
        public int TypeId { get; set; }
    }

基本上,一个状态对象可以包含零个或多个视频或图片。日志对象还可以包含零个或多个视频,声音或图片。以下是用于实现此目的的流利的api代码:

class LogEntityTypeConfiguration : IEntityTypeConfiguration<Log>
    {
        public void Configure(EntityTypeBuilder<Log> logConfiguration)
        {
            logConfiguration.HasMany(b => b.Videos)
              .WithOne()
              .HasForeignKey("TypeId")
              .OnDelete(DeleteBehavior.Cascade);

            logConfiguration.HasMany(b => b.Photos)
              .WithOne()
              .HasForeignKey("TypeId")
              .OnDelete(DeleteBehavior.Cascade);

            logConfiguration.HasMany(b => b.Audios)
              .WithOne()
              .HasForeignKey("TypeId")
              .OnDelete(DeleteBehavior.Cascade);
        }
    }

-

public void Configure(EntityTypeBuilder<Status> statusConfiguration)
    {
        statusConfiguration.HasMany(b => b.Videos)
          .WithOne()
          .HasForeignKey("TypeId")
          .OnDelete(DeleteBehavior.Cascade);

        statusConfiguration.HasMany(b => b.Photos)
          .WithOne()
          .HasForeignKey("TypeId")
          .OnDelete(DeleteBehavior.Cascade);
    }

这构建得很好,下图显示了生成的外键。 enter image description here

我有一个日志存储库类,当尝试插入日志对象时,出现以下错误:

  

System.Data.SqlClient.SqlException(0x80131904):插入   语句与FOREIGN KEY约束冲突
  “ FK_Photos_Statuses_TypeId”。数据库“ xxxx”中发生了冲突,   表“ dbo.Statuses”的“ Id”列

public async Task<Log> AddAsync(Log log, LogFiles files)
    {
        var strategy = _context.Database.CreateExecutionStrategy();

        await strategy.ExecuteAsync(async () => {

            using (var txn = _context.Database.BeginTransaction())
            {
                try
                {
                    if (log.IsTransient())
                    {
                        _context.Logs.Add(log);
                        _context.SaveChanges();

                        if (files.Video != null)
                        {
                            Video vid = new Video();
                            vid = files.Video;
                            log._videos.Add(vid);
                        }

                        if(files.Picture != null)
                        {
                            Photo ph = new Photo();
                            ph = files.Picture;
                            log._photos.Add(ph);
                        }

                        if(files.Audio != null)
                        {
                            Sound aud = new Sound();
                            aud = files.Audio;
                            log._audios.Add(aud);
                        }
                        _context.SaveChanges();

                        txn.Commit();
                    }

                }
                catch (Exception ex)
                {
                    txn.Rollback();
                }
            }

        });

        return log;
    }

当我尝试插入日志对象时,我也不明白为什么错误列表中会显示状态对象的外键?

p.s,如果您有更好的方法可以为这种关系建模,请分享。

1 个答案:

答案 0 :(得分:0)

这很可能是由于实体分离。 Log&LogFiles属性的来源是什么?我的猜测是这些来自网络客户端吗?

要概述传递实体的问题: 让我们看一下具有状态参考的Photo实体。

public class Status
{
   public int StatusId { get; set; }
   public string Name { get; set; }
}
public class Photo
{
   public int PhotoId { get; set; }
   public virtual Status Status { get; set; }
}

现在,如果我从DbContext加载一组照片,我可能会带回2张状态为“新建”的照片。

就“实例”而言,我将:

 Photo (ID: 1)  \
                  ==> Status (ID: 1 [New])
 Photo (ID: 2)  /

问题是,当我将那些断开连接的实体发送回Controller之类的东西时,它们已反序列化,并且看起来像下面的样子:

 Photo (ID: 1)  ==> Status (ID: 1 [New])
 Photo (ID: 2)  ==> Status (ID: 1 [New])

在您的情况下,您要传回新照片(精细),但应将其与现有状态相关联。您的Photo实体可能会设置为生成PK,但不会显示类似Status的查找。无论哪种方式,如果EF都不“了解”状态,它将与照片一起被视为新实体。当EF尝试插入状态ID 1时,这会导致FK约束。

将实体传递回控制器会导致各种问题。例如,如果您正在执行照片编辑,则传回照片ID 1,您会发现您需要以某种方式告诉EF关于照片#1(例如,使用Attach并将状态设置为“已修改”),然后还会遇到与照片相关的所有实体周围的FK错误。附加相关的实体(例如Status)将首先解决您的问题,但随后会导致进一步的复杂性,如上述,对同一Status的多个引用实际上是Status对象的单独实例。在第一个实例上调用attach将起作用,但是一旦在该上下文中以相同状态保存某些内容,您将立即获得异常。如果您尝试附加第二个引用,则不同的引用和EF会抱怨具有相同ID的实例与上下文相关联。

从客户端本身附加实体是一种危险的做法,因为您隐式地信任从客户端返回的数据。精明的恶意用户可以轻松地以超出网页允许范围的任何方式修改数据,从而破坏数据存储。

我给围绕EF的开发人员的主要建议是“不要传递实体”。没有什么好事了。 :)如果传递日志视图模型,照片视图模型等,那么它将减少在服务器和客户端之间来回传送的数据量(使系统更快且资源占用更少),并迫使您考虑即将到来的数据背部。

例如,如果我取回LogInsertViewModel和一组关联的PhotoInsertViewModels

public async Task<Log> AddAsync(LogInsertViewModel logVm, ICollection<PhotoInsertViewModel> photoVms)
{
   // TODO: Validate that the log & files are correct/complete and applicable to the current session user...

   // If I need to lookup values from collections... (1 hit to DB to get all applicable)
   statusIds = photoVms.Select(x => x.StatusId).ToList();
   var statuses = context.Statuses.Where(x => statusIds.Contains(x.StatusId)).ToList();

   // Alternatively if I know all new photos were going to be associated a "New" status...
   var newStatus = context.Statuses.Single(x => x.Status = Statuses.New);

   // Create a Log.
   var log = new Log
   {
      //.. copy values.


      Photos = photoVms.Select(x => new Photo
      {
         // copy values.
         Status = statuses.Single(s => s.StatusId = x.StatusId); // or newStatus
      }).ToList();
   };
   context.Logs.Add(log);
   context.SaveChanges();
}