web-dev-qa-db-fra.com

Comment puis-je forcer le cadre d'une entité à insérer des colonnes d'identité?

Je veux écrire du code C # pour initialiser ma base de données avec des données de départ. Clairement, cela nécessitera la possibilité de pouvoir définir les valeurs de différentes colonnes d'identité lors de l'insertion. J'utilise une approche code-first. Par défaut, DbContext gère la connexion à la base de données et vous ne pouvez donc pas SET IDENTITY_INSERT [dbo].[MyTable] ON. Donc, ce que j’ai fait jusqu’à présent, c’est d’utiliser le constructeur DbContext qui me permet de spécifier une connexion à une base de données. Ensuite, je règle IDENTITY_INSERT Sur ON dans cette connexion à la base de données, puis j'essaie d'insérer mes enregistrements à l'aide du cadre d'entité. Voici un exemple de ce que j'ai jusqu'à présent:

public class MyUserSeeder : IEntitySeeder {
    public void InitializeEntities(AssessmentSystemContext context, SqlConnection connection) {
        context.MyUsers.Add(new MyUser { MyUserId = 106, ConceptPersonId = 520476, Salutation = "Mrs", Firstname = "Novelette", Surname = "Aldred", Email = null, LoginId = "520476", Password="28c923d21b68fdf129b46de949b9f7e0d03f6ced8e9404066f4f3a75e115147489c9f68195c2128e320ca9018cd711df", IsEnabled = true, SpecialRequirements = null });
        try {
            connection.Open();
            SqlCommand cmd = new SqlCommand("SET IDENTITY_INSERT [dbo].[MyUser] ON", connection);
            int retVal = cmd.ExecuteNonQuery();
            context.SaveChanges();
        }
        finally {
            connection.Close();
        }
    }
}

Si proche et pourtant si loin - parce que, bien que cmd.ExecuteNonQuery() fonctionne correctement, lorsque je lance ensuite context.SaveChanges(), je suis informé que "la valeur explicite doit être spécifiée pour la colonne identité de la table 'MyUser 'lorsque IDENTITY_INSERT est défini sur ON ou lorsqu'un utilisateur de réplication est inséré dans une colonne d'identité NOT FOR REPLICATION. "

Vraisemblablement, étant donné que MyUserId (qui est la colonne Identity de la table MyUser) est la clé primaire, le framework d'entité n'essaie pas de le définir lorsque j'appelle context.SaveChanges(), même si j'ai donné le MyUserentité une valeur pour la propriété MyUserId.

Existe-t-il un moyen de forcer le cadre d’entités à essayer d’insérer même les valeurs de clé primaire d’une entité? Ou peut-être un moyen de marquer temporairement MyUserId comme n'étant pas une valeur de clé primaire, alors EF essaie de l'insérer?

51
Jez

Après mûre réflexion, j'ai décidé que le refus d'insérer des colonnes d'identité par la structure de l'entité était une fonctionnalité et non un bogue. :-) Si je devais insérer toutes les entrées dans ma base de données, y compris leurs valeurs d'identité, je devrais aussi créer une entité pour chaque table de liens que le cadre d'entité avait automatiquement créé pour moi! Ce n'est tout simplement pas la bonne approche.

Donc, ce que je fais, c'est configurer des classes d'ensemencement qui utilisent simplement du code C # et créent des entités EF, puis utilisez un DbContext pour enregistrer les données nouvellement créées. Il faut un peu plus de temps pour transformer le code SQL déchargé en code C #, mais il n'y a pas (et ne devrait pas être) trop de données juste pour "semer" des données - il devrait s'agir d'une petite quantité de données représentative. type de données qui figureraient dans une base de données dynamique pouvant être rapidement insérées dans une nouvelle base de données à des fins de débogage/développement. Cela signifie que si je veux lier des entités entre elles, je dois faire des requêtes sur ce qui a déjà été inséré, sinon mon code ne connaîtra pas leur valeur d'identité générée, par exemple. Ce genre de chose apparaîtra dans le code d’implantation, après que j’ai installé et terminé context.SaveChanges pour MyRoles:

var roleBasic = context.MyRoles.Where(rl => rl.Name == "Basic").First();
var roleAdmin = context.MyRoles.Where(rl => rl.Name == "Admin").First();
var roleContentAuthor = context.MyRoles.Where(rl => rl.Name == "ContentAuthor").First();

MyUser thisUser = context.MyUsers.Add(new MyUser {
    Salutation = "Mrs", Firstname = "Novelette", Surname = "Aldred", Email = null, LoginUsername = "naldred", Password="c1c966821b68fdf129c46de949b9f7e0d03f6cad8ea404066f4f3a75e11514748ac9f68695c2128e520ca0275cd711df", IsEnabled = true, SpecialRequirements = null
});
thisUser.Roles.Add(roleBasic);

En procédant de cette façon, il est également plus probable que je mette à jour mes données d’amorçage lorsque je modifierai le schéma, car je romprai probablement le code d’amorçage lorsque je le modifierai (si je supprime un champ ou une entité, le code d’amorçage existant qui utilise ce champ)./entity ne parviendra pas à compiler). Avec un script SQL pour effectuer un amorçage, ce ne serait pas le cas et le script SQL ne serait pas non plus agnostique à la base de données.

Je pense donc que si vous essayez de définir les champs d’identité des entités pour la gestion des données de la base de données, vous avez définitivement choisi la mauvaise approche.

Si je faisais glisser une charge de données de SQL Server vers PostgreSQL (une base de données complète, et pas seulement des données générées), je pourrais le faire via EF, mais je souhaiterais ouvrir deux contextes en même temps, et écrivez du code pour saisir toutes les diverses entités du contexte source et les placer dans le contexte de destination, puis enregistrez les modifications.

Généralement, le seul moment approprié pour insérer des valeurs d'identité est lorsque vous copiez d'une base de données vers une autre dans le même SGBD (SQL Server -> SQL Server, PostgreSQL -> PostgreSQL, etc.), puis vous le feriez tout d'abord dans un script SQL et non pas dans le code EF d'abord (le script SQL ne serait pas agnostique à la base de données, mais ce ne serait pas nécessaire; vous n'allez pas entre différents SGBD).

2
Jez

EF 6 en utilisant la article msdn :

    using (var dataContext = new DataModelContainer())
    using (var transaction = dataContext.Database.BeginTransaction())
    {
      var user = new User()
      {
        ID = id,
        Name = "John"
      };

      dataContext.Database.ExecuteSqlCommand("SET IDENTITY_INSERT [dbo].[User] ON");

      dataContext.User.Add(user);
      dataContext.SaveChanges();

      dataContext.Database.ExecuteSqlCommand("SET IDENTITY_INSERT [dbo].[User] OFF");

      transaction.Commit();
    }

Mise à jour: Pour éviter les erreurs "Une valeur explicite doit être spécifiée pour la colonne d'identité dans la table 'Nom de table' lorsque IDENTITY_INSERT est défini sur ON ou lorsqu'un utilisateur de réplication est insertion dans une colonne d'identité NOT FOR REPLICATION ", vous devez modifier la valeur de la propriété StoreGeneratedPattern de la colonne identité de Identity en Aucun dans le concepteur de modèle.

Veuillez noter que la modification de StoreGeneratedPattern en Aucune échouera lors de l'insertion d'un objet sans l'ID spécifié (manière normale) avec l'erreur "Impossible d'insérer une valeur explicite pour la colonne d'identité dans la table 'NomTable' lorsque IDENTITY_INSERT est défini sur OFF".

46
Roman O

Vous n'avez pas besoin de faire de drôles de business avec la connexion, vous pouvez couper l'homme du milieu et simplement utiliser ObjectContext.ExecuteStoreCommand .

Vous pouvez alors réaliser ce que vous voulez en faisant ceci:

context.ExecuteStoreCommand("SET IDENTITY_INSERT [dbo].[MyUser] ON");

Je ne connais pas de moyen intégré pour dire à EF d'activer l'insertion d'identité.

Ce n'est pas parfait, mais ce serait plus flexible et moins "hacky" que votre approche actuelle.

Mise à jour:

Je viens de me rendre compte que votre problème comporte une deuxième partie. Maintenant que vous avez dit à SQL que vous souhaitiez créer des insertions d'identité, EF n'essaie même pas d'insérer des valeurs pour ladite identité (pourquoi le ferions-nous? Nous ne lui avons pas dit).

Je n'ai aucune expérience de la première approche de code, mais d'après certaines recherches rapides, il semble que vous deviez dire à EF que votre colonne ne devrait pas être générée à partir du magasin. Vous aurez besoin de faire quelque chose comme ça.

Property(obj => obj.MyUserId)
    .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None)
    .HasColumnName("MyUserId");

J'espère que cela vous dirigera dans la bonne direction :-)

18
Doctor Jones

Peu de temps en retard pour la fête, mais au cas où quelqu'un rencontrerait ce problème dans EF5 avec DB en premier: je ne pouvais pas faire fonctionner l'une ou l'autre solution, mais j'ai trouvé une autre solution de contournement:

Avant d'exécuter la commande .SaveChanges(), je réinitialise le compteur d'identité de la table:

Entities.Database.ExecuteSqlCommand(String.Format("DBCC CHECKIDENT ([TableNameHere], RESEED, {0})", newObject.Id-1););
Entities.YourTable.Add(newObject);
Entities.SaveChanges();

Cela signifie que .SaveChanges() doit être appliqué après chaque addition - mais au moins cela fonctionne!

11
Peter Albert

Voici la solution du problème. Je l'ai essayé sur EF6 et cela a fonctionné pour moi. Voici un pseudo-code qui devrait fonctionner.

Tout d’abord, vous devez créer la surcharge du dbcontext par défaut. Si vous vérifiez la classe de base, vous trouverez celle qui passe avec succès dbConnection. Vérifiez le code suivant

public MyDbContext(DbConnection existingConnection, bool contextOwnsConnection)
        : base(existingConnection, contextOwnsConnection = true)
    {
        //optional
        this.Configuration.ProxyCreationEnabled = true;
        this.Configuration.LazyLoadingEnabled = true;
        this.Database.CommandTimeout = 360;
    }

Et dans modelcreating, supprimez l’option générée par la base de données telle que,

protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<MyTable>()
            .Property(a => a.Id)
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);

        base.OnModelCreating(modelBuilder);
    }

Maintenant, dans le code, vous devez explicitement passer un objet de connexion,

using (var connection = new System.Data.SqlClient.SqlConnection(ConfigurationManager.ConnectionStrings["ConnectionStringName"].ConnectionString))
        {
            connection.Open();
            using (var context = new MyDbContext(connection, true))
            {
                context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT [dbo].[MyTable] ON");
                context.MyTable.AddRange(objectList);
                context.SaveChanges();
                context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT [dbo].[MyTable] OFF");
            }

            connection.Close();
        }
6
Pavvy

Cette idée ne fonctionne de manière fiable que si la table cible est vide ou si des enregistrements sont insérés avec des identifiants supérieurs à tous les identifiants déjà existants dans la table!

Trois ans plus tard, je rencontrais un problème similaire lors du transfert des données de production dans un système de test. Les utilisateurs voulaient pouvoir copier les données de production dans le système de test quand ils le voulaient. Au lieu de configurer un travail de transfert dans SQL Server, j'ai donc cherché un moyen d'effectuer le transfert dans l'application à l'aide des classes EF existantes. De cette façon, je pourrais fournir aux utilisateurs un élément de menu pour démarrer le transfert quand ils le voudraient.

L'application utilise une base de données MS SQL Server 2008 et EF 6. Comme les deux bases de données ont généralement la même structure, je pensais pouvoir facilement transférer des données d'une instance de DbContext à une autre en lisant les enregistrements de chaque entité à l'aide de AsNoTracking(). et juste Add() (ou AddRange()) les enregistrements de la propriété appropriée sur l'instance cible de DbContext.

Voici un DbContext avec une entité à illustrer:

public class MyDataContext: DbContext
{
    public virtual DbSet<Person> People { get; set; }
}

Pour copier les données de personnes j'ai fait ce qui suit:

private void CopyPeople()
{
    var records = _sourceContext.People.AsNoTracking().ToArray();
    _targetContext.People.AddRange(records);
    _targetContext.SaveChanges();
}

Tant que les tables ont été copiées dans le bon ordre (pour éviter les problèmes de contraintes de clé étrangère), cela a très bien fonctionné. Malheureusement, les tables utilisant des colonnes d'identité rendaient les choses un peu difficiles, car EF ignorait les valeurs id et laissait simplement SQL Server insérer la valeur d'identité suivante. Pour les tables avec des colonnes d'identité, j'ai fini par:

  1. Lire tous les enregistrements d'une entité donnée
  2. Ordonner les enregistrements par ID en ordre croissant
  3. définir la graine d'identité de la table sur la valeur du premier identifiant
  4. en gardant une trace de la valeur d’identité suivante, ajoutez les enregistrements un à un. Si l'ID n'est pas identique à la valeur d'identité suivante attendue, définissez le germe d'identité sur la valeur requise suivante.

Tant que la table est vide (ou que tous les nouveaux enregistrements ont des identifiants plus élevés que leur identifiant actuel), et que les identifiants sont dans un ordre croissant, EF et MS SQL insèrent les identifiants requis et aucun système ne se plaindra.

Voici un peu de code pour illustrer:

private void InsertRecords(Person[] people)
{
    // setup expected id - presumption: empty table therefore 1
    int expectedId = 1;

    // now add all people in order of ascending id
    foreach(var person in people.OrderBy(p => p.PersonId))
    {
        // if the current person doesn't have the expected next id
        // we need to reseed the identity column of the table
        if (person.PersonId != expectedId)
        {
            // we need to save changes before changing the seed value
            _targetContext.SaveChanges();

            // change identity seed: set to one less than id
            //(SQL Server increments current value and inserts that)
            _targetContext.Database.ExecuteSqlCommand(
                String.Format("DBCC CHECKIDENT([Person], RESEED, {0}", person.PersonId - 1)
            );

            // update the expected id to the new value
            expectedId = person.PersonId;
        }

        // now add the person
        _targetContext.People.Add(person);

        // bump up the expectedId to the next value
        // Assumption: increment interval is 1
        expectedId++;
    }

    // now save any pending changes
    _targetContext.SaveChanges();
}

En utilisant la réflexion, j'ai pu écrire une méthode Load et une méthode Save qui fonctionnaient pour toutes les entités du DbContext.

C'est un peu un bidouillage, mais cela me permet d'utiliser les méthodes EF standard pour lire et écrire des entités et résout le problème de la définition des colonnes d'identité avec des valeurs particulières dans un ensemble de circonstances données.

J'espère que cela aidera quelqu'un d'autre confronté à un problème similaire.

2
roadkill

Après avoir expérimenté plusieurs options trouvées sur ce site, le code suivant a fonctionné pour moi (EF 6). Notez qu'il tente d'abord une mise à jour normale si l'élément existe déjà. Si ce n'est pas le cas, essayez une insertion normale, si l'erreur est due à IDENTITY_INSERT, puis essayez la solution de contournement. Notez également que db.SaveChanges échouera, d’où l’instruction db.Database.Connection.Open () et l’étape de vérification facultative. Sachez que ce n'est pas mettre à jour le contexte, mais dans mon cas, ce n'est pas nécessaire. J'espère que cela t'aides!

public static bool UpdateLeadTime(int ltId, int ltDays)
{
    try
    {
        using (var db = new LeadTimeContext())
        {
            var result = db.LeadTimes.SingleOrDefault(l => l.LeadTimeId == ltId);

            if (result != null)
            {
                result.LeadTimeDays = ltDays;
                db.SaveChanges();
                logger.Info("Updated ltId: {0} with ltDays: {1}.", ltId, ltDays);
            }
            else
            {
                LeadTime leadtime = new LeadTime();
                leadtime.LeadTimeId = ltId;
                leadtime.LeadTimeDays = ltDays;

                try
                {
                    db.LeadTimes.Add(leadtime);
                    db.SaveChanges();
                    logger.Info("Inserted ltId: {0} with ltDays: {1}.", ltId, ltDays);
                }
                catch (Exception ex)
                {
                    logger.Warn("Error captured in UpdateLeadTime({0},{1}) was caught: {2}.", ltId, ltDays, ex.Message);
                    logger.Warn("Inner exception message: {0}", ex.InnerException.InnerException.Message);
                    if (ex.InnerException.InnerException.Message.Contains("IDENTITY_INSERT"))
                    {
                        logger.Warn("Attempting workaround...");
                        try
                        {
                            db.Database.Connection.Open();  // required to update database without db.SaveChanges()
                            db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT[dbo].[LeadTime] ON");
                            db.Database.ExecuteSqlCommand(
                                String.Format("INSERT INTO[dbo].[LeadTime]([LeadTimeId],[LeadTimeDays]) VALUES({0},{1})", ltId, ltDays)
                                );
                            db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT[dbo].[LeadTime] OFF");
                            logger.Info("Inserted ltId: {0} with ltDays: {1}.", ltId, ltDays);
                            // No need to save changes, the database has been updated.
                            //db.SaveChanges(); <-- causes error

                        }
                        catch (Exception ex1)
                        {
                            logger.Warn("Error captured in UpdateLeadTime({0},{1}) was caught: {2}.", ltId, ltDays, ex1.Message);
                            logger.Warn("Inner exception message: {0}", ex1.InnerException.InnerException.Message);
                        }
                        finally
                        {
                            db.Database.Connection.Close();
                            //Verification
                            if (ReadLeadTime(ltId) == ltDays)
                            {
                                logger.Info("Insertion verified. Workaround succeeded.");
                            }
                            else
                            {
                                logger.Info("Error!: Insert not verified. Workaround failed.");
                            }
                        }
                    }
                }
            }
        }
    }
    catch (Exception ex)
    {
        logger.Warn("Error in UpdateLeadTime({0},{1}) was caught: {2}.", ltId.ToString(), ltDays.ToString(), ex.Message);
        logger.Warn("Inner exception message: {0}", ex.InnerException.InnerException.Message);
        Console.WriteLine(ex.Message);
        return false;
    }
    return true;
}
1
David