簡體   English   中英

為什么 Entity Framework Core 試圖將記錄從多對多關系而不是連接表插入到其中一個表中?

[英]Why is Entity Framework Core attempting to insert records into one of the tables from many to many relationships and NOT the join table?

鑒於以下設置,其中有許多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時就會出現問題。 我在新的TeamSession上添加一個新的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);
        }
`

這不是多對多的關系。

它是兩個獨立的一對多關系,恰好在關系的一端引用同一張表。

雖然在數據庫級別,這兩個用例都由三個表表示,即Foo 1->* FooBar *<-1 Bar ,但這兩種情況在實體框架的自動化行為中被區別對待 - 這非常重要。

如果是直接的多對多,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會看foobar實體,並相應地處理它們。 EF 足夠聰明,可以意識到您希望bar存儲在數據庫中,因為您將它放在foo對象中,並且您明確要求 EF 將foo添加到數據庫中。

重要的是要意識到您已經告訴 EF 應該創建foo (因為您調用了Add() ,這意味着一個新項目),但您從未告訴 EF 它應該對bar做什么。 目前尚不清楚(對 EF)您希望 EF 對此做什么,因此 EF 只能猜測要做什么。

如果您從未向 EF 解釋bar是否已存在,則實體框架默認假設它需要在數據庫中創建此實體

保存新的 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 屬性而不是導航道具,您通知 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()時將新值提交到數據庫。


請注意,與解決方案 2 相比,我更喜歡解決方案 1,因為它是萬無一失的,並且不太可能導致意外行為或運行時異常。


字符串主鍵是不可取的

我不會說它不起作用,但實體框架無法自動生成字符串,這使得它們不適合作為實體 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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM