Pomelo.entityframeworkcore.mysql: L'ajout d'une entité à la base de données avec la colonne "Id" prédéfinie entraîne la génération d'un identifiant incorrect dans la base de données

Créé le 27 mai 2020  ·  4Commentaires  ·  Source: PomeloFoundation/Pomelo.EntityFrameworkCore.MySql

Étapes à reproduire

Classe de domaine :

    public class Category
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

Configuration de l'entité :

    public class CategoryConfiguration : IEntityTypeConfiguration<Category>
    {
        public void Configure(EntityTypeBuilder<Category> entity)
        {
            entity.HasKey(c => c.Id);

            entity.Property(c => c.Id).IsRequired().ValueGeneratedOnAdd();
            entity.Property(c => c.Name).IsRequired().HasMaxLength(55);
        }
    }

Lieu où j'ajoute une entité à la base de données (CategoryDto est identique à Category)

        [HttpPost]
        public IActionResult CreateCategory([FromBody] CategoryDto categoryDto)
        {
            var category = _mapper.Map<Category>(categoryDto);

            _unitOfWork.Categories.Add(category);
            _unitOfWork.Complete();

            _mapper.Map(category, categoryDto);
            return Created($"{Request.Path}/{category.Id}", categoryDto);
        }

Vous pouvez consulter le code complet ici :

Le problème

Lorsque j'envoie une demande POST avec une colonne d'ID prédéfinie (par exemple 1000 ou 15), ce nombre prédéfini sera l'ID de l'entité ajoutée dans la base de données. De plus, tous les autres identifiants commenceront à partir de cette valeur.
error

Comportement prévisible

Quel que soit l'ID dans le champ, lors de la création d'un enregistrement dans la base de données, la valeur de la colonne doit être définie correctement.

Plus de détails techniques

Version de MySQL : 8.0.19-mysql
Système d'exploitation : Windows 10
Pomelo.EntityFrameworkCore.MySql version : 3.1.1
Version de l'application Microsoft.AspNetCore. : 3.1.201

type-question

Commentaire le plus utile

MySQL utilisera n'importe quelle valeur d'ID que vous fournissez. Seulement si vous ne fournissez pas de valeur, MySQL générera une auto_increment pour vous. Lorsqu'il génère un ID, il utilisera la valeur la plus élevée déjà présente dans la table (ou 0 sinon) comme graine.

Jetez un œil à 3.6.9 Utilisation d'AUTO_INCREMENT dans la documentation MySQL :

L'attribut AUTO_INCREMENT peut être utilisé pour générer une identité unique pour les nouvelles lignes :
[...]
Aucune valeur n'a été spécifiée pour la colonne AUTO_INCREMENT, MySQL a donc attribué automatiquement des numéros de séquence. Vous pouvez également affecter explicitement 0 à la colonne pour générer des numéros de séquence
[...]
Si la colonne est déclarée NON NULL, il est également possible d'affecter NULL à la colonne pour générer des numéros de séquence.
[...]
Lorsque vous insérez une autre valeur dans une colonne AUTO_INCREMENT, la colonne est définie sur cette valeur et la séquence est réinitialisée de sorte que la prochaine valeur générée automatiquement suive séquentiellement la plus grande valeur de colonne.

Le code suivant montre que :

```c#
en utilisant System.Diagnostics;
en utilisant System.Linq ;
en utilisant Microsoft.EntityFrameworkCore ;
en utilisant Microsoft.Extensions.Logging ;

espace de noms IssueConsoleTemplate
{
Glace en classe publique
{
public int IceCreamId { get; ensemble; }
chaîne publique Nom { get; ensemble; }
}

public class Context : DbContext
{
    public DbSet<IceCream> IceCreams { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseMySql(
                "server=127.0.0.1;port=3306;user=root;password=;database=Issue1092",
                b => b.ServerVersion("8.0.20-mysql"))
            .UseLoggerFactory(
                LoggerFactory.Create(b => b
                        .AddConsole()
                        .AddFilter(level => level >= LogLevel.Information)))
            .EnableSensitiveDataLogging()
            .EnableDetailedErrors();
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<IceCream>(
            entity =>
            {
                // Because the property contains "Id", the following lines are added automatically,
                // so no need to specify them explicitly (does not do any harm however):
                // entity.Property(e => e.IceCreamId)
                //    .ValueGeneratedOnAdd();
            });
    }
}

internal class Program
{
    private static void Main()
    {
        using var context = new Context();

        context.Database.EnsureDeleted();
        context.Database.EnsureCreated();

        context.IceCreams.AddRange(
            new IceCream
            {
                IceCreamId = 42, // explicitly set ID
                Name = "Vanilla",
            },
            new IceCream
            {
                // <-- let MySQL use it's auto_increment behavior
                Name = "Chocolate",
            });

        context.SaveChanges();

        var iceCreams = context.IceCreams
            .OrderBy(i => i.IceCreamId)
            .ToList();

        Debug.Assert(iceCreams.Count == 2);
        Debug.Assert(iceCreams[0].IceCreamId == 42);
        Debug.Assert(iceCreams[0].Name == "Vanilla");
        Debug.Assert(iceCreams[1].IceCreamId == 43);
        Debug.Assert(iceCreams[1].Name == "Chocolate");
    }
}

}


It generates the following SQL:

```sql
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core 3.1.3 initialized 'Context' using provider 'Pomelo.EntityFrameworkCore.MySql' with options: ServerVersion 8.0.20 MySql SensitiveDataLoggingEnabled DetailedErrorsEnabled

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (12ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE DATABASE `Issue1092`;

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (56ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE `IceCreams` (
          `IceCreamId` int NOT NULL AUTO_INCREMENT,
          `Name` longtext CHARACTER SET utf8mb4 NULL,
          CONSTRAINT `PK_IceCreams` PRIMARY KEY (`IceCreamId`)
      );

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (8ms) [Parameters=[@p0='42', @p1='Vanilla' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      INSERT INTO `IceCreams` (`IceCreamId`, `Name`)
      VALUES (<strong i="6">@p0</strong>, @p1);

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (4ms) [Parameters=[@p0='Chocolate' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      INSERT INTO `IceCreams` (`Name`)
      VALUES (@p0);
      SELECT `IceCreamId`
      FROM `IceCreams`
      WHERE ROW_COUNT() = 1 AND `IceCreamId` = LAST_INSERT_ID();

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT `i`.`IceCreamId`, `i`.`Name`
      FROM `IceCreams` AS `i`
      ORDER BY `i`.`IceCreamId`

Ainsi, le comportement que vous rencontrez est attendu et est conforme à la documentation.

Solution possible:

Pour résoudre votre problème, réinitialisez simplement votre Category.Id à 0 avant d'appeler SaveChanges() .

Puisque vous utilisez un modèle de référentiel explicite, une solution simple consisterait simplement à remplacer la méthode Add() :

```c#
classe abstraite publique: IRéférentieloù TEntity : classe
{
public virtual void Add(TEntity entity) // <-- rendu virtuel
{
Contexte.Set().Ajouter(entité);
}
}

classe publique CategoryRepository : Repository, ICategoryRepository
{
public override void Ajouter (entité de catégorie)
{
entité.Id = 0;
base.Ajouter(entité);
}
}
```

Tous les 4 commentaires

Pouvez-vous partager le schéma MySQL de la table Category ? Le comportement dont vous avez besoin n'est pas tout à fait clair, est-ce que l'identifiant est fourni par l'utilisateur ou généré automatiquement dans la base de données avec auto_increment ? Si c'est le cas, vous n'avez pas besoin de ValueGeneratedOnAdd Method .

Pouvez-vous partager le schéma MySQL de la table Category ? Le comportement dont vous avez besoin n'est pas tout à fait clair, est-ce que l'identifiant est fourni par l'utilisateur ou généré automatiquement dans la base de données avec auto_increment ? Si c'est le cas, vous n'avez pas besoin de ValueGeneratedOnAdd Method .

image

Eh bien, par exemple, lorsque j'utilise MS SQL, si j'envoie une requête POST avec des données comme celle-ci :

{
    "Id":12137123,
    "Name":"Some Category"
}

cet identifiant aléatoire sera omis lors de la création de l'entrée dans la base de données et correct sera défini dans la colonne ID. Le but de tout cela est que si un utilisateur malveillant essaie de mettre un mauvais identifiant, ma base de données sera cassée

MySQL utilisera n'importe quelle valeur d'ID que vous fournissez. Seulement si vous ne fournissez pas de valeur, MySQL générera une auto_increment pour vous. Lorsqu'il génère un ID, il utilisera la valeur la plus élevée déjà présente dans la table (ou 0 sinon) comme graine.

Jetez un œil à 3.6.9 Utilisation d'AUTO_INCREMENT dans la documentation MySQL :

L'attribut AUTO_INCREMENT peut être utilisé pour générer une identité unique pour les nouvelles lignes :
[...]
Aucune valeur n'a été spécifiée pour la colonne AUTO_INCREMENT, MySQL a donc attribué automatiquement des numéros de séquence. Vous pouvez également affecter explicitement 0 à la colonne pour générer des numéros de séquence
[...]
Si la colonne est déclarée NON NULL, il est également possible d'affecter NULL à la colonne pour générer des numéros de séquence.
[...]
Lorsque vous insérez une autre valeur dans une colonne AUTO_INCREMENT, la colonne est définie sur cette valeur et la séquence est réinitialisée de sorte que la prochaine valeur générée automatiquement suive séquentiellement la plus grande valeur de colonne.

Le code suivant montre que :

```c#
en utilisant System.Diagnostics;
en utilisant System.Linq ;
en utilisant Microsoft.EntityFrameworkCore ;
en utilisant Microsoft.Extensions.Logging ;

espace de noms IssueConsoleTemplate
{
Glace en classe publique
{
public int IceCreamId { get; ensemble; }
chaîne publique Nom { get; ensemble; }
}

public class Context : DbContext
{
    public DbSet<IceCream> IceCreams { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseMySql(
                "server=127.0.0.1;port=3306;user=root;password=;database=Issue1092",
                b => b.ServerVersion("8.0.20-mysql"))
            .UseLoggerFactory(
                LoggerFactory.Create(b => b
                        .AddConsole()
                        .AddFilter(level => level >= LogLevel.Information)))
            .EnableSensitiveDataLogging()
            .EnableDetailedErrors();
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<IceCream>(
            entity =>
            {
                // Because the property contains "Id", the following lines are added automatically,
                // so no need to specify them explicitly (does not do any harm however):
                // entity.Property(e => e.IceCreamId)
                //    .ValueGeneratedOnAdd();
            });
    }
}

internal class Program
{
    private static void Main()
    {
        using var context = new Context();

        context.Database.EnsureDeleted();
        context.Database.EnsureCreated();

        context.IceCreams.AddRange(
            new IceCream
            {
                IceCreamId = 42, // explicitly set ID
                Name = "Vanilla",
            },
            new IceCream
            {
                // <-- let MySQL use it's auto_increment behavior
                Name = "Chocolate",
            });

        context.SaveChanges();

        var iceCreams = context.IceCreams
            .OrderBy(i => i.IceCreamId)
            .ToList();

        Debug.Assert(iceCreams.Count == 2);
        Debug.Assert(iceCreams[0].IceCreamId == 42);
        Debug.Assert(iceCreams[0].Name == "Vanilla");
        Debug.Assert(iceCreams[1].IceCreamId == 43);
        Debug.Assert(iceCreams[1].Name == "Chocolate");
    }
}

}


It generates the following SQL:

```sql
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core 3.1.3 initialized 'Context' using provider 'Pomelo.EntityFrameworkCore.MySql' with options: ServerVersion 8.0.20 MySql SensitiveDataLoggingEnabled DetailedErrorsEnabled

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (12ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE DATABASE `Issue1092`;

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (56ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE `IceCreams` (
          `IceCreamId` int NOT NULL AUTO_INCREMENT,
          `Name` longtext CHARACTER SET utf8mb4 NULL,
          CONSTRAINT `PK_IceCreams` PRIMARY KEY (`IceCreamId`)
      );

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (8ms) [Parameters=[@p0='42', @p1='Vanilla' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      INSERT INTO `IceCreams` (`IceCreamId`, `Name`)
      VALUES (<strong i="6">@p0</strong>, @p1);

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (4ms) [Parameters=[@p0='Chocolate' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      INSERT INTO `IceCreams` (`Name`)
      VALUES (@p0);
      SELECT `IceCreamId`
      FROM `IceCreams`
      WHERE ROW_COUNT() = 1 AND `IceCreamId` = LAST_INSERT_ID();

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT `i`.`IceCreamId`, `i`.`Name`
      FROM `IceCreams` AS `i`
      ORDER BY `i`.`IceCreamId`

Ainsi, le comportement que vous rencontrez est attendu et est conforme à la documentation.

Solution possible:

Pour résoudre votre problème, réinitialisez simplement votre Category.Id à 0 avant d'appeler SaveChanges() .

Puisque vous utilisez un modèle de référentiel explicite, une solution simple consisterait simplement à remplacer la méthode Add() :

```c#
classe abstraite publique: IRéférentieloù TEntity : classe
{
public virtual void Add(TEntity entity) // <-- rendu virtuel
{
Contexte.Set().Ajouter(entité);
}
}

classe publique CategoryRepository : Repository, ICategoryRepository
{
public override void Ajouter (entité de catégorie)
{
entité.Id = 0;
base.Ajouter(entité);
}
}
```

Merci pour la réponse et l'aide

Cette page vous a été utile?
0 / 5 - 0 notes