Permalänk
Medlem

[Q] C# - EF relation

Så jag har en situation där jag har två modeller (klasser):

public class Game { [Key] public int Id { get; set; } public string Name { get; set; } public int DeveloperId { get; set; } public string Description { get; set; } public int PublishedYear { get; set; } public List<Rating> Ratings { get; set; } public List<Genre> Genres { get; set; } // <------ denna som jag misslyckas med }

public class Genre { [Key] public int Id { get; set; } public bool KidsFriendly { get; set; } = true; public string Name { get; set; } }

Jag exekverar en kod där jag har en collection av spel som har en collection i sig av Genre, ett exempel:

var games = new List<Game>() { new Game() { Name = "<namn>", Description = "<beskrivning>", Genres = new List<Genre>(){ GenreController.GetOneGenreByName("<genre namn>"), GenreController.GetOneGenreByName("<genre namn>")}, PublishedYear = <år>, Ratings = new List<Rating>(), //utebliven för nu, då jag inte lyckas få genre att fungera ens DeveloperId = <utvecklare>.Id } }

Dold text

db.Games.AddRange(games); db.SaveChanges();

Jag får två problem, när jag kör AddRange får jag felmeddelandet att:

Citat:

An unexpected error occured in the creation phase of games: The instance of entity type 'Genre' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values. ().

Om jag kör en gammal hederlig Add får jag:

Citat:

An unexpected error occured in the creation phase of games: An error occurred while updating the entries. See the inner exception for details. (MySqlConnector.MySqlException (0x80004005): Duplicate entry '94' for key 'Genre.PRIMARY'
at MySqlConnector.Core.ResultSet.ReadResultSetHeaderAsync(IOBehavior ioBehavior) in /_/src/MySqlConnector/Core/ResultSet.cs:line 50
at MySqlConnector.MySqlDataReader.ActivateResultSet(CancellationToken cancellationToken) in /_/src/MySqlConnector/MySqlDataReader.cs:line 137
at MySqlConnector.MySqlDataReader.CreateAsync(CommandListPosition commandListPosition, ICommandPayloadCreator payloadCreator, IDictionary`2 cachedProcedures, IMySqlCommand command, CommandBehavior behavior, IOBehavior ioBehavior, CancellationToken cancellationToken) in /_/src/MySqlConnector/MySqlDataReader.cs:line 445
at MySqlConnector.Core.CommandExecutor.ExecuteReaderAsync(IReadOnlyList`1 commands, ICommandPayloadCreator payloadCreator, CommandBehavior behavior, IOBehavior ioBehavior, CancellationToken cancellationToken) in /_/src/MySqlConnector/Core/CommandExecutor.cs:line 60
at MySqlConnector.MySqlCommand.ExecuteReaderAsync(CommandBehavior behavior, IOBehavior ioBehavior, CancellationToken cancellationToken) in /_/src/MySqlConnector/MySqlCommand.cs:line 314
at MySqlConnector.MySqlCommand.ExecuteDbDataReader(CommandBehavior behavior) in /_/src/MySqlConnector/MySqlCommand.cs:line 256
at System.Data.Common.DbCommand.ExecuteReader()
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReader(RelationalCommandParameterObject parameterObject)
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.Execute(IRelationalConnection connection)).

Dold text

Min avsedda relation är att:

  • Spel kan ha oändligt antal mot Genre

  • Genre ska kunna appliceras utan att skapas på nytt

Databasens tabeller över de två:

Games

CREATE TABLE `Games` ( `Id` int NOT NULL AUTO_INCREMENT, `Name` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci, `DeveloperId` int DEFAULT NULL, `Description` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci, `PublishedYear` int NOT NULL, PRIMARY KEY (`Id`), KEY `IX_Games_DeveloperId` (`DeveloperId`), CONSTRAINT `FK_Games_Developer_DeveloperId` FOREIGN KEY (`DeveloperId`) REFERENCES `Developer` (`Id`) ON DELETE RESTRICT ) ENGINE=InnoDB AUTO_INCREMENT=120 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci |

 
EF

migrationBuilder.CreateTable( name: "Games", columns: table => new { Id = table.Column<int>(type: "int", nullable: false) .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), Name = table.Column<string>(type: "longtext", nullable: true) .Annotation("MySql:CharSet", "utf8mb4"), DeveloperId = table.Column<int>(type: "int", nullable: false), Description = table.Column<string>(type: "longtext", nullable: true) .Annotation("MySql:CharSet", "utf8mb4"), PublishedYear = table.Column<int>(type: "int", nullable: false) }, constraints: table => { table.PrimaryKey("PK_Games", x => x.Id); }) .Annotation("MySql:CharSet", "utf8mb4");

Dold text
Dold text

Genre

MySQL

CREATE TABLE `Genre` ( `Id` int NOT NULL AUTO_INCREMENT, `KidsFriendly` tinyint(1) NOT NULL, `Name` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci, `GameId` int DEFAULT NULL, PRIMARY KEY (`Id`), KEY `IX_Genre_GameId` (`GameId`), CONSTRAINT `FK_Genre_Games_GameId` FOREIGN KEY (`GameId`) REFERENCES `Games` (`Id`) ON DELETE RESTRICT ) ENGINE=InnoDB AUTO_INCREMENT=103 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

 
EF

migrationBuilder.CreateTable( name: "Genre", columns: table => new { Id = table.Column<int>(type: "int", nullable: false) .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), KidsFriendly = table.Column<bool>(type: "tinyint(1)", nullable: false), Name = table.Column<string>(type: "longtext", nullable: true) .Annotation("MySql:CharSet", "utf8mb4"), GameId = table.Column<int>(type: "int", nullable: true) }, constraints: table => { table.PrimaryKey("PK_Genre", x => x.Id); table.ForeignKey( name: "FK_Genre_Games_GameId", column: x => x.GameId, principalTable: "Games", principalColumn: "Id", onDelete: ReferentialAction.Restrict); }) .Annotation("MySql:CharSet", "utf8mb4");

Dold text
Dold text

TLDR;
Varför varje gång jag skapar ett Game-object mot databasen så försöker den skapa mina Genre-objekt samtidigt? Jag antar att en foreign key saknas eller att jag behöver sätta upp relationen i OnModelCreating?

Visa signatur

Citera om du vill ha svar, hjälpte jag dig, gilla svaret!

Permalänk
Medlem

Inte satt mig in i din kod helt, men det ser väl lite ut som att du har en ett-till-många-relation mot genre, istället för en många-till-många som det låter att du vill ha? Kör du code first borde väl typ en public ICollection<Game> Games i genre-klassen göra det åt dig (du kan vilja finjustera det hela i modelbuildern iofs, se https://docs.microsoft.com/en-us/ef/core/modeling/relationshi... )

Visa signatur

The power of GNU compiles you!
"Often statistics are used as a drunken man uses lampposts -- for support rather than illumination."

Permalänk
Medlem

Det var så jag tänkte också, men i mitt huvud blir det konstigt när det gäller Entity Framework till varför man skulle lägga upp det så (svårt att visualisera det).

Ska kika vad som händer om jag gör så.

Visa signatur

Citera om du vill ha svar, hjälpte jag dig, gilla svaret!

Permalänk
Medlem

I din modell finns det ingen koppling från Genre och Game, men i din modell och SQL har du med GameId som nullable, vart kommer den från?

Ett spel kan ha flera genres, en genre kan tillhöra flera spel. Alltså är det en många till många relation, dvs. en kopplingstabell.
Alltså vill du ha en collection property på båda sidorna. Din Genre ska ha en ICollection<Game>. EF kommer då i bakgrunden att skapa en kopplingstabell mellan dessa. Förut behövde man mappa upp en kopplingstabell manuellt även i EF, t.ex. en lista av GameGenre. Men från och med EF 5 slipper man detta.

Du kan läsa mer här https://docs.microsoft.com/en-us/ef/core/modeling/relationshi...

Key attributen är inget som behövs heller, EF är smart nog att räkna ut själv att om det finns en egenskap som heter Id som är en int så är det primary key.

Permalänk
Medlem

Eftersom jag inte orkar provköra din kod har jag inget exakt svar. Men här är några tips:

  • När du ber om hjälp eller surfar efter svar var tydlig med om det är .NET Core EF eller den gamla .NET Framework-versionen du använder, det är inte samma API. Exakt version kan också spela roll.

  • Testa att använda public virtual ICollection<T> för relationerna i stället för public List<T>. Ramverket kommer behöva använda genererade proxyklasser som ärver från dina klasser.

  • Felmeddelandena du får är ju ganska tydliga med avseende på vad som händer i databasen. Om du avser att skapa ett nytt objekt så behöver du se till att PK-kolumnen (Id) får ett unikt värde. Enklast brukar vara att dekorera [Key]-propertyn (Id) med [DatabaseGenerated], men man kan sätta värdet själv också naturligtvis. I så fall är det enklast att ha Guid som typ på Id-kolumnen och bara köra Guid.NewGuid().

1:N-relationer sätts upp automatiskt genom konvention om man gör rätt, inklusive foreign keys. För 1:1-relationer så behöver man ta till fluent-API:et i vissa fall. N:M-relationer har jag dålig koll på i EF Core, så där väljer jag att inte säga något, men jag vill minnas att .NET Framework fixar sådana genom konvention (och utan att skapa en fulklass för kopplingstabellen).

Permalänk
Medlem

Så, felet låg inte ens i det, men jag implementerade det ändå, för det är rimligt.
Problemet var att jag använde flera db-kontexter samtidigt.

Så lösningen var helt enkelt att eliminera alla instanser av db-kontexten och köra på en enda igenom hela flödet. Det fanns totalt 3 instanser, satte ihop alla de till en och då funkade allt helt felfritt.

Visa signatur

Citera om du vill ha svar, hjälpte jag dig, gilla svaret!

Permalänk
Medlem
Skrivet av freddyfresh:

Så, felet låg inte ens i det, men jag implementerade det ändå, för det är rimligt.
Problemet var att jag använde flera db-kontexter samtidigt.

Så lösningen var helt enkelt att eliminera alla instanser av db-kontexten och köra på en enda igenom hela flödet. Det fanns totalt 3 instanser, satte ihop alla de till en och då funkade allt helt felfritt.

Vet inte om du kör dotnet 5 men rekommenderar du skapar en context via dependency injection i såna fall. Använd metoden AddDbContext. Då kommer du alltid ha samma en.