【问题标题】:Why is Entity Framework Core attempting to insert records into one of the tables from many to many relationships and NOT the join table?为什么 Entity Framework Core 试图将记录插入到多对多关系的表之一而不是连接表中?
【发布时间】:2019-12-31 00:13:15
【问题描述】:

给定以下设置,其中有很多 Teams 和很多 LeagueSessions。每个Team 属于零个或多个LeagueSessions,但只有一个LeagueSession 处于活动状态。 LeagueSessions有很多团队,团队会重复。在TeamsLeagueSessions 之间建立了多对多关系,连接表名为TeamsSessions

Team 模型如下所示:

public class Team
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public League League { get; set; }
        public string LeagueID { get; set; }        
        public bool Selected { get; set; }
        public ICollection<Match> Matches { get; set; }
        public virtual ICollection<TeamSession> TeamsSessions { get; set; }
    }

团队模型fluent api配置:

`
public class TeamConfiguration
    {        
        public TeamConfiguration(EntityTypeBuilder<Team> model)
        {
            // The data for this model will be generated inside ThePLeagueDataCore.DataBaseInitializer.DatabaseBaseInitializer.cs class
            // When generating data for models in here, you have to provide it with an ID, and it became mildly problematic to consistently get
            // a unique ID for all the teams. In ThePLeagueDataCore.DataBaseInitializer.DatabaseBaseInitializer.cs we can use dbContext to generate
            // unique ids for us for each team.

            model.HasOne(team => team.League)
                .WithMany(league => league.Teams)
                .HasForeignKey(team => team.LeagueID);   
        }

    }
`

每个团队都属于一个 League。联赛模型如下所示:

`public class League
    {
        public string Id { get; set; }
        public string Type { get; set; }
        public string Name { get; set; }
        public IEnumerable<Team> Teams { get; set; }
        public bool Selected { get; set; }        
        public string SportTypeID { get; set; }
        public SportType SportType { get; set; }
        public IEnumerable<LeagueSessionSchedule> Sessions { get; set; }

    }`

League 的流畅 API:

`public LeagueConfiguration(EntityTypeBuilder<League> model)
        {
            model.HasOne(league => league.SportType)
                .WithMany(sportType => sportType.Leagues)
                .HasForeignKey(league => league.SportTypeID);

            model.HasMany(league => league.Teams)
                .WithOne(team => team.League)
                .HasForeignKey(team => team.LeagueID);

            model.HasData(leagues);
        }`

SessionScheduleBase 类如下所示:

public class SessionScheduleBase
    {
        public string LeagueID { get; set; }
        public bool ByeWeeks { get; set; }
        public long? NumberOfWeeks { get; set; }
        public DateTime SessionStart { get; set; }
        public DateTime SessionEnd { get; set; }
        public ICollection<TeamSession> TeamsSessions { get; set; } = new Collection<TeamSession>();
        public ICollection<GameDay> GamesDays { get; set; } = new Collection<GameDay>();
    }

注意:LeagueSessionSchedule 继承自 SessionScheduleBase

TeamSession 模型如下所示:

`public class TeamSession
    {
        public string Id { get; set; }
        public string TeamId { get; set; }
        public Team Team { get; set; }
        public string LeagueSessionScheduleId { get; set; }
        public LeagueSessionSchedule LeagueSessionSchedule { get; set; }
    }`

然后我像这样配置与 fluent API 的关系:

`public TeamSessionConfiguration(EntityTypeBuilder<TeamSession> model)
        {

            model.HasKey(ts => new { ts.TeamId, ts.LeagueSessionScheduleId });            
            model.HasOne(ts => ts.Team)
                .WithMany(t => t.TeamsSessions)
                .HasForeignKey(ts => ts.TeamId);
            model.HasOne(ts => ts.LeagueSessionSchedule)
                .WithMany(s => s.TeamsSessions)
                .HasForeignKey(ts => ts.LeagueSessionScheduleId);
        }`

每当我尝试插入新的LeagueSessionSchedule 时,就会出现问题。我在新的LeagueSessionSchedule 上添加新的TeamSession 对象的方式是这样的:

`foreach (TeamSessionViewModel teamSession in newSchedule.TeamsSessions)
    {                 
        Team team = await this._teamRepository.GetByIdAsync(teamSession.TeamId, ct);

            if(team != null)
            {
                TeamSession newTeamSession = new TeamSession()
                {
                    Team = team,                            
                    LeagueSessionSchedule = leagueSessionSchedule
                };

                leagueSessionSchedule.TeamsSessions.Add(newTeamSession);
            }
    }`

保存新的LeagueSessionSchedule 代码:

public async Task<LeagueSessionSchedule> AddScheduleAsync(LeagueSessionSchedule newLeagueSessionSchedule, CancellationToken ct = default)
{
    this._dbContext.LeagueSessions.Add(newLeagueSessionSchedule);
    await this._dbContext.SaveChangesAsync(ct);

    return newLeagueSessionSchedule;
}

保存新的 LeagueSessionSchedule 对象会引发 Entity Framework Core 的错误,即它无法将重复的主键值插入到 dbo.Teams 表中。我不知道为什么它试图添加到dbo.Teams 表而不是TeamsSessions 表。

错误:

INSERT INTO [LeagueSessions] ([Id], [Active], [ByeWeeks], [LeagueID], [NumberOfWeeks], [SessionEnd], [SessionStart])
VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6);
INSERT INTO [Teams] ([Id], [Discriminator], [LeagueID], [Name], [Selected])
VALUES (@p7, @p8, @p9, @p10, @p11),
(@p12, @p13, @p14, @p15, @p16),
(@p17, @p18, @p19, @p20, @p21),
(@p22, @p23, @p24, @p25, @p26),
(@p27, @p28, @p29, @p30, @p31),
(@p32, @p33, @p34, @p35, @p36),
(@p37, @p38, @p39, @p40, @p41),
(@p42, @p43, @p44, @p45, @p46);

System.Data.SqlClient.SqlException (0x80131904): Violation of PRIMARY KEY constraint 'PK_Teams'. Cannot insert duplicate key in object 'dbo.Teams'. The duplicate key value is (217e2e11-0603-4239-aab5-9e2f1d3ebc2c).

我的目标是创建一个新的LeagueSessionSchedule 对象。随着这个对象的创建,我还必须为连接表创建一个新的TeamSession 条目(或者如果不需要连接表,则不需要),然后能够选择任何给定的团队并查看它当前是哪个会话一部分。

我的整个PublishSchedule 方法如下:

`
public async Task<bool> PublishSessionsSchedulesAsync(List<LeagueSessionScheduleViewModel> newLeagueSessionsSchedules, CancellationToken ct = default(CancellationToken))
        {
            List<LeagueSessionSchedule> leagueSessionOperations = new List<LeagueSessionSchedule>();

            foreach (LeagueSessionScheduleViewModel newSchedule in newLeagueSessionsSchedules)
            {
                LeagueSessionSchedule leagueSessionSchedule = new LeagueSessionSchedule()
                {
                    Active = newSchedule.Active,
                    LeagueID = newSchedule.LeagueID,
                    ByeWeeks = newSchedule.ByeWeeks,
                    NumberOfWeeks = newSchedule.NumberOfWeeks,
                    SessionStart = newSchedule.SessionStart,
                    SessionEnd = newSchedule.SessionEnd                    
                };

                // leagueSessionSchedule = await this._sessionScheduleRepository.AddScheduleAsync(leagueSessionSchedule, ct);

                // create game day entry for all configured game days
                foreach (GameDayViewModel gameDay in newSchedule.GamesDays)
                {
                    GameDay newGameDay = new GameDay()
                    {
                        GamesDay = gameDay.GamesDay
                    };

                     // leagueSessionSchedule.GamesDays.Add(newGameDay);

                    // create game time entry for every game day
                    foreach (GameTimeViewModel gameTime in gameDay.GamesTimes)
                    {
                        GameTime newGameTime = new GameTime()
                        {
                            GamesTime = DateTimeOffset.FromUnixTimeSeconds(gameTime.GamesTime).DateTime.ToLocalTime(),
                            // GameDayId = newGameDay.Id
                        };

                        // newGameTime = await this._sessionScheduleRepository.AddGameTimeAsync(newGameTime, ct);                        
                        newGameDay.GamesTimes.Add(newGameTime);
                    }

                    leagueSessionSchedule.GamesDays.Add(newGameDay);
                }

                // update teams sessions
                foreach (TeamSessionViewModel teamSession in newSchedule.TeamsSessions)
                {
                    // retrieve the team with the corresponding id
                    Team team = await this._teamRepository.GetByIdAsync(teamSession.TeamId, ct);

                    if(team != null)
                    {
                        TeamSession newTeamSession = new TeamSession()
                        {
                            Team = team,                            
                            LeagueSessionSchedule = leagueSessionSchedule
                        };

                        leagueSessionSchedule.TeamsSessions.Add(newTeamSession);
                    }
                }

                // update matches for this session
                foreach (MatchViewModel match in newSchedule.Matches)
                {
                    Match newMatch = new Match()
                    {
                        DateTime = match.DateTime,
                        HomeTeamId = match.HomeTeam.Id,
                        AwayTeamId = match.AwayTeam.Id,
                        LeagueID = match.LeagueID                        
                    };

                    leagueSessionSchedule.Matches.Add(newMatch);
                }

                try
                {
                    leagueSessionOperations.Add(await this._sessionScheduleRepository.AddScheduleAsync(leagueSessionSchedule, ct));
                }
                catch(Exception ex)
                {

                }
            }

            // ensure all leagueSessionOperations did not return any null values
            return leagueSessionOperations.All(op => op != null);
        }
`

【问题讨论】:

  • edit 您的问题包括其余代码,例如如何使用EntityTypeBuilder 配置其他实体以及您如何读取和保存实体。特别是 team 对象来自哪里,因为实体框架认为它是一个新对象。尝试提供问题的minimal reproducible example
  • 它很可能不知道team 已经存在。您的 team 对象可能与 dbcontext 分离,因此它假定需要添加它,因为您已将其声明为 HasOne 要求。
  • 感谢您的回复,我已经添加了附加代码。我认为这应该足够了。如果不是,请现在告诉我。对于连接表TeamSession,我遵循了关于如何设置此处描述的多对多关系的指南learnentityframeworkcore.com/configuration/…
  • @O.MeeKoh _teamRepository 的代码是什么以及您在哪里/如何使用 AddScheduleAsync() 方法。您只显示部分代码并在执行路径中留下漏洞。我们应该看到你的代码从哪里开始,如何打开上下文,它如何读取实体(使用你拥有/使用的帮助类),你如何更改数据以及最后如何保存它们。
  • @ErikPhilips 是正确的。我的 dbContext 类的生命周期是瞬态的,当我将其更改为单例时,它工作得很好。

标签: c# entity-framework-core


【解决方案1】:

这不是多对多的关系。

这是两个独立的一对多关系,恰好在关系的一端引用同一张表。

虽然在数据库级别,这两个用例都由三个表表示,即Foo 1-&gt;* FooBar *&lt;-1 Bar,但 Entity Framework 的自动化行为对这两个用例的处理方式有所不同 - 这非常重要。

如果是直接多对多,EF 只会为您处理交叉表,例如

public class Foo
{
    public virtual ICollection<Bar> Bars { get; set; }
}

public class Bar
{
    public virtual ICollection<Foo> Foos { get; set; }
}

EF 在幕后处理交叉表,您永远不会意识到交叉表的存在(从代码的角度来看)。

重要的是,EF Core 还不支持隐式交叉表!目前在 EF Core 中没有办法做到这一点,但即使有,你也不会使用它,因此无论您使用的是 EF 还是 EF Core,您的问题的答案都是一样的。

但是,您已经定义了自己的交叉表。虽然这仍然代表数据库术语中的多对多关系,但就 EF 而言,它已不再是多对多关系,并且您在 EF 的多对多关系中找到的任何文档都没有更长的时间适用于您的方案。


未附加但间接添加的对象被假定为新对象。

通过“间接添加”,我的意思是您将它作为另一个实体的一部分添加到上下文中(您直接添加到上下文中)。在下面的例子中,foo是直接添加的,bar是间接添加的:

var foo = new Foo();
var bar = new Bar();

foo.Bar = bar;

context.Foos.Add(foo);   // directly adding foo
                         // ... but not bar
context.SaveChanges();

当您向上下文添加(并提交)新实体时,EF 会为您添加它。但是,EF 还会查看第一个实体包含的任何相关实体。在上述示例中的提交期间,EF 将查看 both foobar 实体并相应地处理它们。 EF 足够聪明,可以意识到您希望将bar 存储在数据库中,因为您将它放在foo 对象中,并且您明确要求EF 将foo 添加到数据库中。

重要的是要意识到您已经告诉 EF 应该创建 foo(因为您调用了 Add(),这意味着一个新项目),但您从未告诉 EF 它应该如何处理 bar。目前尚不清楚(对 EF 而言)您希望 EF 对此做什么,因此 EF 只能猜测该做什么。

如果您从未向 EF 解释过 bar 是否已经存在,Entity Framework 默认假设它需要在数据库中创建此实体

保存新的 LeagueSessionSchedule 对象会引发 Entity Framework Core 的错误,即它无法将重复的主键值插入到 dbo.Teams 表中。我不知道为什么它试图添加到 dbo.Teams 表中

知道了你现在所知道的,错误就会变得更清楚。 EF 正在尝试添加此团队(在我的示例中为 bar 对象),因为它没有关于此团队对象及其在数据库中的状态的信息。

这里有一些解决方案。

1.使用 FK 属性而不是导航属性

这是我的首选解决方案,因为它没有出错的余地。如果团队 ID 尚不存在,则会收到错误消息。 EF 绝不会尝试创建团队,因为它甚至不知道团队的数据,它只知道您尝试与之建立关系的(所谓的)ID。

注意:我省略了 LeagueSessionSchedule,因为它与当前错误无关 - 但对于 TeamLeagueSessionSchedule,它的行为基本相同。

TeamSession newTeamSession = new TeamSession()
{
    TeamId = team.Id                           
};

通过使用 FK 属性而不是 nav 属性,您通知 EF 这是一个现有团队 - 因此 EF 不再尝试(重新)创建这个团队。

2。确保团队被当前上下文跟踪

注意:我省略了 LeagueSessionSchedule,因为它与当前错误无关 - 但对于 TeamLeagueSessionSchedule,它的行为基本相同。

context.Teams.Attach(team);

TeamSession newTeamSession = new TeamSession()
{
    Team = team
};

通过将对象附加到上下文中,您可以告知它它的存在。新附加实体的默认状态是Unchanged,意思是“这已经存在于数据库中并且没有被更改——所以当我们提交上下文时你不需要更新它”。

如果您确实对团队进行了更改并希望在提交期间进行更新,则应改为使用:

context.Entry(team).State = EntityState.Modified;

Entry() 本身也附加实体,通过将其状态设置为 Modified,您可以确保在调用 SaveChanges() 时将新值提交到数据库。


请注意,我更喜欢解决方案 1 而不是解决方案 2,因为它是万无一失的,而且不太可能导致意外行为或运行时异常。


字符串主键是不可取的

我不会说它不起作用,但是实体框架不能自动生成字符串,这使得它们不适合作为实体 PK 的类型。您需要手动设置实体 PK 值。

就像我说的,这并非不可能,但您的代码表明您没有明确设置 PK 值:

if(team != null)
{
    TeamSession newTeamSession = new TeamSession()
    {
        Team = team,                            
        LeagueSessionSchedule = leagueSessionSchedule
    };

    leagueSessionSchedule.TeamsSessions.Add(newTeamSession);
}

如果您希望自动生成 PK,请使用适当的类型。 intGuid 是迄今为止最常用的类型。

否则,您将不得不开始设置自己的 PK 值,因为如果您不这样做(并且 Id 值因此默认为 null),当您添加使用上述代码的第二个 TeamSession 对象(即使您正在正确执行其他所有操作),因为 PK null 已被您添加到表中的第一个实体占用。

【讨论】:

  • 您确定 EntityFramework Core 的交叉表吗?我正在查看这个 github 问题以及我发布的指南。 github.com/aspnet/EntityFrameworkCore/issues/1368 。这意味着我需要明确创建“交叉”表
  • @O.MeeKoh:EF Core 还不支持隐式交叉表,这是正确的。这是一个正在进行的功能。但是关于这一点被观察为两个单独的一对多关系代表了EF的either版本。我会修改我的答案以指出这一点,但答案的核心保持不变。
  • 我同意你的帖子,我喜欢解决方案一而不是解决方案二,但是如果新联赛尚未保存到数据库中,我该如何添加 LeagueSessionScheduleId = leagueSessionSchedule.Id。除非我先保存再执行这个操作。我的印象是我可以将未保存的 LeagueSessionSchedule 添加到未保存的 TeamSession 的导航属性中,并且在保存新的 LeagueSessionSchedule 后,EF Core 会自动保留并添加 LeagueSessionScheduleId,因为我设置了导航属性。
  • @O.MeeKoh 如果需要添加leagueSessionSchedule 确实,那么team 的当前行为(由于团队已经存在而引发错误)是leagueSessionSchedule 的正确方法(确实需要创建,因为它尚不存在)。您的问题未明确说明您对leagueSessionSchedule 的预期方法,我无法解释您未提供的信息
  • @O.MeeKoh:与您当前的代码场景无关,我使用Guid 作为我的实体 ID,并将我的实体设置为在其构造函数中自动生成一个新的 Guid - 当你这样做时,你可以 实际使用 FK,即使您正在处理添加但尚未保存的实体。它现在并不直接适用于您的情况,但如果您决定放弃基于字符串的 PK,它可能会变得相关。
猜你喜欢
  • 1970-01-01
  • 2022-12-10
  • 1970-01-01
  • 2018-06-24
  • 1970-01-01
  • 1970-01-01
  • 2018-03-26
  • 1970-01-01
  • 2018-09-25
相关资源
最近更新 更多