使用完整上下文
我目前有两个看起来如下的实体。
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);
});
答案 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 字段。 (在加载实体然后跨值复制的情况下,仅会更改您更改的那些值)