[英]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
有很多球隊,而且球隊會重復。 Teams
和LeagueSessions
之間建立了多對多關系,並使用名為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會看都foo
和bar
實體,並相應地處理它們。 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
因為它與當前錯誤無關 - 但它對於Team
和LeagueSessionSchedule
行為本質上是相同的。
TeamSession newTeamSession = new TeamSession()
{
TeamId = team.Id
};
通過使用 FK 屬性而不是導航道具,您通知 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()
時將新值提交到數據庫。
請注意,與解決方案 2 相比,我更喜歡解決方案 1,因為它是萬無一失的,並且不太可能導致意外行為或運行時異常。
我不會說它不起作用,但實體框架無法自動生成字符串,這使得它們不適合作為實體 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
對象時,您的代碼將失敗(即使您正確地執行了其他所有操作),因為您添加到表中的第一個實體已經采用了 PK null
。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.