web-dev-qa-db-fra.com

Comment faire en sorte que EF 6 gère DEFAULT CONSTRAINT sur une base de données pendant INSERT

Je suis nouveau sur EF (c'est ma première semaine), mais pas sur les bases de données ni sur la programmation. D'autres ont posé des questions similaires, mais je ne pense pas que les détails nécessaires ont été demandés ou expliqués comme il se doit, alors j'y vais.

Question: Comment faire en sorte qu'Entity Framework traite correctement les colonnes d'une base de données ayant une contrainte DEFAULT définie lors de l'exécution d'un INSERT? En d'autres termes, si je ne fournis pas de valeur dans mon modèle lors d'une opération d'insertion, comment EF peut-il exclure cette colonne de la commande TSQL INSERT générée, de sorte que DEFAULT CONSTRAINT défini par la base de données fonctionne?

Contexte

J'ai créé un tableau simple, juste pour tester Entity Framework 6 (EF6) et son interaction avec les colonnes que SQL Server est capable de mettre à jour. Ceci utilise IDENTITY, TIMESTAMP, COMPUTED et quelques colonnes avec une contrainte par défaut appliquée.

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[DBUpdateTest](
    [RowID] [int] IDENTITY(200,1) NOT NULL,
    [UserValue] [int] NOT NULL,
    [DefValue1] [int] NOT NULL,
    [DefValue2null] [int] NULL,
    [DefSecond] [int] NOT NULL,
    [CalcValue]  AS 
        (((([rowid]+[uservalue])+[defvalue1])+[defvalue2null])*[defsecond]),
    [RowTimestamp] [timestamp] NULL,
    CONSTRAINT [PK_DBUpdateTest] PRIMARY KEY CLUSTERED 
    (
        [RowID] ASC
    )
    WITH 
    (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF,
    ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) 
) 
GO
ALTER TABLE [dbo].[DBUpdateTest] 
ADD CONSTRAINT [DF_DBUpdateTest_DefValue1]      
DEFAULT ((200)) FOR [DefValue1]
GO
ALTER TABLE [dbo].[DBUpdateTest] 
ADD CONSTRAINT [DF_DBUpdateTest_DefValue2null]  
DEFAULT ((30)) FOR [DefValue2null]
GO
ALTER TABLE [dbo].[DBUpdateTest] 
ADD  CONSTRAINT [DF_DBUpdateTest_DefSecond]  
DEFAULT (datepart(second,getdate())) FOR [DefSecond]
GO

EF6 gère parfaitement les colonnes IDENTITY, TIMESTAMP et COMPUTED, ce qui signifie qu'après INSERT ou UPDATE (via context.SaveChanges()), EF lit les nouvelles valeurs dans l'objet entité pour une utilisation immédiate.

Cependant, cela ne se produit pas pour les colonnes avec DEFAULT CONSTRAINT. Et d'après ce que je peux dire, c'est parce que, lorsque EF génère le TSQL pour exécuter le INSERT, il fournit la valeur par défaut commune pour les types nullable ou non nullable, comme si cette colonne ne contenait pas de DEFAULT CONSTRAINT. Il semble donc évident que EF ignore complètement la possibilité d’une contrainte par défaut.

Voici mon code EF pour INSÉRER un enregistrement DBUpdateTest (et je ne mets à jour qu'une seule colonne):

DBUpdateTest myVal = new DBUpdateTest();
myVal.UserValue = RND.Next(20, 90);
DB.DBUpdateTests.Add(myVal);
DB.SaveChanges();

Voici le code SQL généré par EF lors d'une instruction INSERT to DBUpdateTest (qui met à jour consciencieusement toutes les colonnes possibles):

 exec sp_executesql 
 N'INSERT [dbo].[DBUpdateTest]([UserValue], [DefValue1], [DefValue2null],
          [DefSecond])
   VALUES (@0, @1, NULL, @2)
   SELECT [RowID], [CalcValue], [RowTimestamp]
   FROM [dbo].[DBUpdateTest]
   WHERE @@ROWCOUNT > 0 AND [RowID] = scope_identity()',
 N'@0 int,@1 int,@2 int',@0=86,@1=0,@2=54

Notez qu'il fournit très clairement ce qui serait la valeur par défaut pour un INT NOT NULL (0) et un INT NULL (null), ce qui surmonte complètement la contrainte par défaut.

C'est ce qui se produit lorsque la commande EF INSERT s'exécute, où elle fournit un NULL pour la colonne nullable et un ZERO pour la colonne INT.

RowID   UserValue   DefValue1   DefValue2null   DefSecond   CalcValue
=========================================================================
211     100         200         NULL            0           NULL

Si, au contraire, j'exécute cette instruction:

insert into DBUpdateTest (UserValue) values (100)

Je vais avoir un disque comme ça

RowID   UserValue   DefValue1   DefValue2null   DefSecond   CalcValue
=========================================================================
211     100         200         30              7           3787

Cela fonctionne comme prévu pour une raison: la commande TSQL INSERT n'a pas fourni de valeurs pour les colonnes avec DEFAULT CONSTRAINT défini.

Par conséquent, j'essaie de faire en sorte que EF exclue les colonnes DEFAULT CONSTRAINT de INSERT TSQL si je ne leur attribue pas explicitement des valeurs dans l'objet de modèle.

Ce que j'ai déjà essayé

1. Reconnaître la contrainte par défaut?SO: Comment faire en sorte que EF gère une contrainte par défaut

Dans la méthode OnModelCreating() de ma classe DbContext, il a été recommandé que je puisse dire à EF qu'une colonne avec DEFAULT CONSTRAINT est un champ COMPUTED, ce qui n'est pas le cas. Cependant, je voulais voir si cela permettrait à EF de lire au moins la valeur après l'INSERT (cela ne m'empêche pas que cela m'empêcherait également de pouvoir affecter une valeur à cette colonne, ce qui correspond davantage à ce que je fais pas envie):

        modelBuilder.Entity<DBUpdateTest>()
            .Property(e => e.DefValue1)
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed);

Cela ne fonctionne pas et ne semble en fait faire rien du tout (ED: en fait, cela fonctionne, voir le point 2). EF génère toujours le même TSQL, fournissant des valeurs par défaut pour les colonnes et neutralisant la base de données dans le processus.

Existe-t-il un indicateur qui me manque, un élément de configuration que je ne sais plus définir, un attribut de fonction que je peux utiliser, un code de classe hérité que je peux créer, pour que EF "gère correctement les colonnes DEFAULT CONSTRAINT?"

2. Exécuter OnModelCreating () pour l'exécuter?SO: OnModelCreating non appelé

Janesh (ci-dessous) m'a montré que EF va éliminer les paramètres de la commande TSQL INSERT générée si la colonne est marquée avec DatabaseGeneratedOption.Computed. Cela ne fonctionnait tout simplement pas pour moi car apparemment, j'utilisais le mauvais type de chaîne de connexion (!!!).

Voici mon App.config, et voici la section <connectionStrings> où je montre la chaîne de connexion "mauvaise" et "bonne":

<connectionStrings>
  <add name="TEST_EF6Entities_NO_WORKY" providerName="System.Data.EntityClient" connectionString="metadata=res://*/TCXModel.csdl|res://*/TCXModel.ssdl|res://*/TCXModel.msl;provider=System.Data.SqlClient;provider connection string=&quot;data source=...ConnectStringHere...;App=EntityFramework&quot;"  />
  <add name="TEST_EF6Entities_IT_WORKS" providerName="System.Data.SqlClient" connectionString="data source=...ConnectStringHere...;App=EntityFramework;"  />
</connectionStrings>

La différence: Celui qui fonctionne utilise ProviderName de System.Data.SqlClient, celui qui ne fonctionne pas utilise System.Data.EntityClient. Apparemment, le fournisseur SqlClient autorise l’appel de la méthode OnModelCreating(), ce qui permet à mon utilisation de DatabaseGeneratedOption.Computed d’avoir un effet.

========== PAS ENCORE RESOLU ===========

Le but de DEFAULT CONSTRAINTS sur les colonnes est de me permettre de fournir (ou non) une valeur, tout en finissant par se retrouver avec une valeur valide du côté base de données. Je n'ai pas besoin de savoir que SQL Server est en train de le faire, pas plus que je ne dois savoir quelle est la valeur par défaut ou ce qu'elle devrait être. Cela se produit complètement en dehors de mon contrôle ou de ma connaissance. 

Le fait est que j’ai l’option de ne pas fournir la valeur. Je peux le fournir, ou je peux ne pas le faire, et je peux le faire différemment pour chaque INSERT si nécessaire.

Utiliser DatabaseGeneratedOption.Computed n’est vraiment pas une option valide dans ce cas car cela force le choix: "vous pouvez TOUJOURS fournir une valeur (et par conséquent, NE JAMAIS utiliser le mécanisme par défaut de la base de données), ou vous ne pouvez JAMAIS fournir une valeur (et donc TOUJOURS utiliser le paramètre par défaut de la base de données)." mécanisme)". 

De plus, cette option est clairement destinée à être utilisée uniquement sur les colonnes calculées, et non sur les colonnes avec DEFAULT CONSTRAINTs, car une fois appliquée, la propriété du modèle devient effectivement READ-ONLY aux fins d'insertion et de mise à jour - car c'est ainsi qu'une vraie colonne calculée travaillerait. Évidemment, cela fait obstacle à mon choix de fournir ou non une valeur à la base de données.

Je demande donc toujours: Comment puis-je faire fonctionner EF correctement "avec" des colonnes de base de données ayant une définition DEFAULT CONSTRAINT?

18
Mike Stillion

Ce bit est la clé de votre question:

Par conséquent, ce que j'essaie de faire, c’est d’obtenir que EF NE PAS inclure les colonnes DEFAULT CONSTRAINT dans son INSERT TSQL si je n’explicitement pas Leur donner des valeurs dans l’objet.

Entity Framework ne fera pas cela pour vous. Les champs sont soit toujours calculés, soit toujours inclus dans les insertions et les mises à jour. Mais vous POUVEZ écrire les classes de se comporter de la manière que vous décrivez. Vous devez définir les champs (explicitement) sur les valeurs par défaut dans le constructeur ou utiliser des champs de sauvegarde. 

public class DBUpdateTest
/* public partial class DBUpdateTest*/ //version for database first
{
   private _DefValue1 = 200;
   private _DefValue2 = 30;

   public DbUpdateTest()
   {
      DefSecond = DateTime.Second;
   }

   public DefSecond { get; set; }

   public DefValue1
   {
      get { return _DefValue1; }
      set { _DefValue1 = value; }
   }

   public DefValue2
   {
      get { return _DefValue2; }
      set { _DefValue2 = value; }
   }
}

Si vous insérez toujours en utilisant ces classes, vous n'avez probablement pas besoin de définir la valeur par défaut dans la base de données, mais si vous insérez en utilisant SQL de l'extérieur, vous devrez également ajouter la contrainte par défaut à la base de données.

5
Colin

Je ne suis pas du tout d'accord avec le fait que DatabaseGeneratedOption.Computed n'aide pas à arrêter l'envoi du champ dans la commande insert SQL. J'ai essayé avec un petit exemple minime à vérifier et cela a fonctionné.

Remarque : Une fois que vous avez appliqué DatabaseGeneratedOption.Computed à une propriété, vous souhaitez pouvoir spécifier n'importe quelle valeur à partir de EF. c'est-à-dire que vous ne pouvez spécifier aucune valeur lors de l'insertion ou de la mise à jour des enregistrements.

Modèle

public class Person
{
    public int Id { get; set; }
    public int SomeId { get; set; }
    public string Name { get; set; }
}

Le contexte

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

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Person>().HasKey(d => d.Id);
        modelBuilder.Entity<Person>()
            .Property(d => d.Id)
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
        modelBuilder.Entity<Person>()
            .Property(d => d.SomeId).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed);
    }
}

Migration

public partial class Initial : DbMigration
{
    public override void Up()
    {
        CreateTable(
            "dbo.People",
            c => new
                {
                    Id = c.Int(nullable: false, identity: true),
                    SomeId = c.Int(nullable: false, defaultValue:3), //I edited it mannually to assign default value 3.
                    Name = c.String(),
                })
            .PrimaryKey(t => t.Id);

    }

    public override void Down()
    {
        DropTable("dbo.People");
    }
}

Note : J'ai modifié manuellement la valeur par défaut 3 en SomeId.

MainProgram :

    static void Main(string[] args)
    {
        using (Context c = new Context())
        {
            Person p = new Person();
            p.Name = "Jenish";
            c.People.Add(p);
            c.Database.Log = Console.WriteLine;
            c.SaveChanges();
        }
    }

La requête suivante a été enregistrée sur ma console:

Opened connection at 04/15/2015 11:32:19 AM +05:30

Started transaction at 04/15/2015 11:32:19 AM +05:30

INSERT [dbo].[People]([Name])
VALUES (@0)
SELECT [Id], [SomeId]
FROM [dbo].[People]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()


-- @0: 'Jenish' (Type = String, Size = -1)

-- Executing at 04/15/2015 11:32:20 AM +05:30

-- Completed in 3 ms with result: SqlDataReader



Committed transaction at 04/15/2015 11:32:20 AM +05:30

Closed connection at 04/15/2015 11:32:20 AM +05:30

Remarquez que certains ID n'ont pas été passés à la commande Insérer. Ils ont été sélectionnés dans la commande select.

2
Jenish Rabadiya

Quelqu'un a répondu à cette question en 2013/2014, je crois :). Le projet utilisait EF5, d'accord, ça fait! Il m'a dit:

  1. Cliquez avec le bouton droit de la souris sur votre .edmx, choisissez "Ouvrir avec", puis "Éditeur de texte (XML)" et recherchez les colonnes que vous avez définies comme contrainte par défaut dans la section <!-- SSDL content --> du fichier.
  2. Ajoutez ensuite StoreGeneratedPattern="Computed" à la fin de ces champs. 

Fait, alors cela a fonctionné. 

Puis je suis passé à EF6 et ça marche TOUJOURS, mais SANS l'addition faite auparavant.  

Maintenant, je suis confronté à un problème très intéressant, avec EF6, version 6.1.3. En ce moment même, j'ai 2 tables avec une contrainte par défaut pour définir un champ booléen sur 1, que l'enregistrement soit ACTIVE ou INACTIVE . Aussi simple que cela.

Dans un tableau, cela fonctionne, c'est-à-dire: je n'ai pas à ajouter ce StoreGeneratedPattern="Computed" à la colonne à l'intérieur de .edmx. Mais, dans l'autre tableau, CELA N'EST PAS; Je dois ajouter ce StoreGeneratedPattern="Computed" à la même colonne que la table 1.

1
Mephisto