首页 > 解决方案 > 为什么 Entity Framework Core 试图将记录插入到多对多关系的表之一而不是连接表中?

问题描述

给定以下设置,其中有很多Teams,有很多LeagueSessions。每个都Team属于零个或多个LeagueSessions,但只有一个LeagueSession是活动的。LeagueSessions有很多团队,并且团队将重复。多对多关系在名为 的连接表Teams之间建立。LeagueSessionsTeamsSessions

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; }

    }`

流利的API League

`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. 我在新TeamSession对象上添加新对象的方式LeagueSessionSchedule是这样的:

`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);
        }
`

标签: c#entity-framework-core

解决方案


这不是多对多的关系。

它是两个独立的一对多关系,碰巧在关系的一端引用同一个表。

虽然在数据库级别上,这两个用例都由三个表表示,即Foo 1->* FooBar *<-1 BarEntity 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 将同时foo查看和bar实体并相应地处理它们。EF 足够聪明,可以意识到您希望bar将其存储在数据库中,因为您将其放在foo对象中并且明确要求 EF 添加foo到数据库中。

重要的是要意识到您已经告诉 EFfoo应该创建(因为您调用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,因为它与当前错误无关 - 但对于Team和基本上是相同的行为LeagueSessionSchedule

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

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

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

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

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,请使用适当的类型。int并且Guid是迄今为止最常用的类型。

否则,您将不得不开始设置自己的 PK 值,因为如果您不这样做(并且该Id值默认为null),那么当您使用上述代码添加第二个对象时,您的代码将失败TeamSession(即使你做的一切都是正确的),因为 PKnull已经被你添加到表中的第一个实体所占用。


推荐阅读