.NET实体框架-重复实体

时间:2020-10-16 12:48:07

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

使用完整上下文

我目前有两个看起来如下的实体。

MovieSerie

    public class MovieSerie
    {
        [Key]
        public Guid MovieSerieId { get; set; }

        [Required]
        [MaxLength(128)]
        public string Title { get; set; }

        [Required]
        [MaxLength(256)]
        public string Description { get; set; }

        public virtual ICollection<Movie> Movies { get; set; }
    }

电影

    public class Movie
    {
        [Key]
        public Guid MovieId { get; set; }

        [Required]
        [MaxLength(128)]
        public string Title { get; set; }

        public virtual MovieSerie MovieSerie { get; set; }
    }

我已删除了一些到目前为止尚未使用的属性,因此该示例更具可读性。

这些实体具有一对多关系,因为MovieSerie包含多部电影,但一部电影只能属于一个MovieSerie。

问题

当我试图通过提供一个现有的MovieSerie从Postman制作一部新电影时,我遇到了异常。异常如下所示。

键'movieseries.PRIMARY'的重复条目'\ xA9 \ xCE \ x0E \ x1E \ x9A \ xAE \ xA2G \ x91 <\ xE6 \ xE3- \ x88C \ xE9'

所以我发现当我提供MovieSerie对象时,它正在尝试制作新的MovieSerie。我尝试从Postman发送的请求中的原始JSON如下所示。

{
    "MovieId" : "6aa8c134-689c-45e2-bf60-cd0eb5473cc2",
    "Title" : "TestMovie",
    "MovieSerie" : {
        "movieSerieId": "1e0ecea9-ae9a-47a2-913c-e6e32d8843e9",
        "title": "Harry Potter",
        "description": "This contains the Harry Potter serie"
    }
}

保存电影的POST方法如下所示。

        [HttpPost]
        public async Task<ActionResult<Movie>> PostMovie(Movie movie)
        {
            if (movie == null)
            {
                return BadRequest("No movie object provided");
            }

            else if (movie.MovieSerie != null)
            {
                if (!_validator.MovieSerieExists(movie.MovieSerie.MovieSerieId))
                {
                    return BadRequest("The movie serie does not exists in the database");
                }
            }

            _context.Movies.Add(movie);
            await _context.SaveChangesAsync();

            return CreatedAtAction("GetMovie", new { id = movie.MovieId }, movie);
        }

有人能告诉我我做错了什么吗?为什么要尝试创建一个已经存在的新实体?我该如何更改才能获得理想的行为?

我试图提供所有必需的信息,但是,如果我错过了什么,请告诉我。

编辑添加的DBCONTEXT

            modelBuilder.Entity<MovieSerie>(entity =>
            {
                entity.HasKey(movieSerie => movieSerie.MovieSerieId);
                entity.Property(movieSerie => movieSerie.Title).IsRequired();
                entity.Property(movieSerie => movieSerie.Description).IsRequired();
                entity.HasMany(ms => ms.Movies)
                .WithOne(m => m.MovieSerie);
            });

            modelBuilder.Entity<Movie>(entity =>
            {
                entity.HasKey(movie => movie.MovieId);
                entity.Property(movie => movie.Title).IsRequired();
                entity.HasOne(m => m.MovieSerie)
                .WithMany(s => s.Movies);
            });

2 个答案:

答案 0 :(得分:0)

您的问题是您要传递整个电影系列对象。这不是您应该做的事情。顾名思义,关系数据库的思想是关联表。这种关系是使用键(外键)完成的。 在您的特定情况下,您需要在Movie表中定义一个外键列,以将其与MovieSeries关联,如下所示:

public class Movie
{
    [Key]
    public Guid MovieId { get; set; }
    public int MovieSerieId {get; set; }
    [Required]
    [MaxLength(128)]
    public string Title { get; set; }

    [ForeignKey("MovieSerieID")]
    public virtual MovieSerie MovieSerie { get; set; }
}

如您所见,我指定MovieSerieID属性为外键。 EF使用虚拟的MovieSerie属性来获取外键的所有详细信息。

现在,您可以创建仅通过MovieSerieid的电影,如下所示:

{
    "MovieId" : "6aa8c134-689c-45e2-bf60-cd0eb5473cc2",
    "Title" : "TestMovie",
    "MovieSerieId": "1e0ecea9-ae9a-47a2-913c-e6e32d8843e9"
}

答案 1 :(得分:0)

这是在ASP.Net中的服务器和客户端之间传递实体时发生的情况。当DbContext的生命周期范围限定为请求时,实体将由DbContext加载并传递到视图,但是随后在Post调用中传递的是一个JSON对象,该对象反序列化为实体类定义。根据请求,DbContext不会跟踪Movie或其关联的相关实体。

当您告诉Post的DbContext添加影片时,该影片上的所有子实体也将被视为 new 实体,从而导致记录重复。

>

如何避免这种情况:

选项1:使用ViewModels避免将来自视图的数据与实体混淆。 (数据状态)这始终是我推荐的选项。这避免了您正在处理的对象的混乱,也意味着您可以减少通过网络发送的数据量。随着实体变大,来回发送实体意味着视图不需要的字段的有效载荷也更大。可以填充ViewModels以仅提供视图将与之交互的字段。 Automapper可以通过ProjectTo方法在很大程度上帮助将实体图转换为ViewModel。

因此,如果我们有一个用于创建电影的视图(电影/创建),并且该视图列出了可供选择的电影系列,则它可能会搜索/获取该系列:

[Serializable]
public class MovieSeriesSummaryViewModel
{
    public Guid MovieSeriesId { get; set; }
    public string Name { get; set; }
}

然后,当控制器去搜索/检索这些系列以供选择时:

var series = _context.MovieSeries
      // .Where(x => [search criteria...])
      .ProjectTo<MovieSeriesSummaryViewModel>(config)
      .ToList();

var series = _context.MovieSeries
      // .Where(x => [search criteria...])
      .Select( x = > new MovieSeriesSummaryViewModel
      {
          MovieSeriesId = x.MovieSeriesId,
          Name = x.Name
      }).ToList();

PostMovie动作接受PostMovieViewModel:

[Serializable]
public class PostMovieViewModel
{
    public string MovieName { get; set; }
    public Guid? MovieSeriesId { get; set; }
    // ... 
}

创建电影视图模型只需要传递系列ID(如果适用)和必填字段即可创建新电影。在创建新电影时,我们从那里将DbContext中的系列相关联:

[HttpPost]
public async Task<ActionResult<PostMovieViewModel>> PostMovie(PostMovieViewModel movieVM)
{
    var movieSeries = movieVM.MovieSeriesId.HasValue 
        ? _context.MovieSeries.Single(x => x.MovieSeriesId == movieVM.MovieSeriesId.Value)
        : null;
    var movie = new Movie
    {
        Name = movieVM.Name,
        MovieSeries = movieSeries
    };
    _context.Movies.Add(movie);
    await _context.SaveChangesAsync();
}

这里的关键点是我们从上下文中获取现有的剧集以关联到新电影。通过ID提取实体的速度非常快,可以作为有意义的验证,证明我们传入的数据是否完整。

选项2:重新关联所有引用。传递反序列化对象并将其视为实体的根本问题是DbContext没有跟踪它们。您可以通过2种方法解决此问题,要么告诉DbContext对其进行跟踪,要么将引用替换为跟踪的对象。

2a-替换参考

[HttpPost]
public async Task<ActionResult<Movie>> PostMovie(Movie movie)
{
     if (movie.MovieSeries != null)
     {
         var existingMovieSeries = _context.MovieSeries
             .Single(x => MovieSeriesId == movie.MovieSeries.MovieSeriesId);
         movie.MovieSeries = existingMovieSeries; // Replace the reference.
     }
     _context.Movies.Add(movie);
     await _context.SaveChanges();
}

这仍然可能意味着要访问数据库以获取所有引用,而忘记这样做将导致无提示的重复问题。

2b-跟踪相关实体。我最后保存的这个文件看似简单,但却会使您绊倒...

[HttpPost]
public async Task<ActionResult<Movie>> PostMovie(Movie movie)
{
     if (movie.MovieSeries != null)
         _context.Attach(movie.MovieSeries);

     _context.Movies.Add(movie);
     await _context.SaveChanges();
}

这看起来很简单,并且在大多数时间都可以使用,但是如果 DbContext已经出于任何原因已经在跟踪该电影系列,则Attach方法将失败。此错误可能会在运行时间歇性出现,具体取决于特定的操作/数据组合。 (即更新2个电影/ w同一系列或有条件地调用加载该系列的方法)正确的检查是:

[HttpPost]
public async Task<ActionResult<Movie>> PostMovie(Movie movie)
{
     if (movie.MovieSeries != null)
     {   
         var existingMovieSeries = _context.MovieSeries.Local
             .SingleOrDefault(x => x.MovieSeriesId == movie.MovieSeries.MovieSeriesId);
         if (existingMovieSeries == null)
             _context.Attach(movie.MovieSeries);
         else
             movie.MovieSeries = existingMovieSeries;
     }
     _context.Movies.Add(movie);
     await _context.SaveChanges();
}

检查MovieSeries.Local来检查DbContext是否正在跟踪该系列。 (不影响数据库),如果没有,我们可以附加它。如果是这样,我们需要替换参考。对于新对象的每个引用,可能要放入很多样板代码。附加来自视图的实体时,重要的是 not 永远不要先验证数据是否有效就将该实体的实体状态设置为Modified。 (无论如何,这都需要先加载实体)。这样做可能会导致用户以您不希望的方式更改数据,因为将实体设置为Modified会更新该实体上的 all 字段。 (在加载实体然后跨值复制的情况下,仅会更改您更改的那些值)