Accueil > .Net, C#, Développement > ADO.Net Entity Data Model et champs avec valeur par défaut

ADO.Net Entity Data Model et champs avec valeur par défaut

Récemment, j’ai eu un problème lors de l’insertion avec Entity Framework.
Le cas est assez simple : je ne spécifiais pas de valeur sur une colonne non nullable mais ayant une contrainte de valeur par défaut.
Et là, bam : « Impossible d’insérer la valeur NULL dans la colonne ‘[COLONNE]’, table ‘[BASE].[SCHEMA].[TABLE]’. Cette colonne n’accepte pas les valeurs NULL. Échec de INSERT. L’instruction a été arrêtée. »

Donc, ce billet va donner le cas de test et les solutions pour le gérer.

 

Génération de l’EDMX après ajout des contraintes

 

Voici ma table en question :

CREATE TABLE [dbo].[MATABLE](
	[PK] [int] IDENTITY(1,1) NOT NULL,
	[COLONNE1] [varchar](50) NULL,
	[COLONNE2] [varchar](50) NULL,
	[COLONNE3] [varchar](50) NULL,
	[CREATION] [datetime] NOT NULL,
 CONSTRAINT [PK_MATABLE] PRIMARY KEY CLUSTERED 
(
	[PK] ASC
))

GO

ALTER TABLE [dbo].[MATABLE]
       ADD  CONSTRAINT [DF_MATABLE_CREATION]  DEFAULT (getdate()) FOR [CREATION]
GO

Rien de bien méchant, donc.

En écrivant un bête :

INSERT INTO [SAMPLES].[dbo].[MATABLE]
           ([COLONNE1],[COLONNE2],[COLONNE3])
     VALUES
           ('COLONNE1','COLONNE2','COLONNE3')

On aura une ligne de créée :
Cas de Test - Insert Sql

Jusque là, tout va bien, donc.

Maintenant, si on génère l’EDMX à partir de la table, les propriétés du champ CREATION seront comme ceci :
Edmx avec nullable à False

Si on joue le simple code suivant :

            using (var context = new SAMPLESEntities())
            {
                var ligne = new MATABLE
                                {
                                    COLONNE1 = "Colonne#1",
                                    COLONNE2 = "Colonne#2",
                                    COLONNE3 = "Colonne#3"
                                };
                context.MATABLE.AddObject(ligne);
                context.SaveChanges();
            }

Ça va planter :
Insertion EF avec Nullable False

C’est réellement pas ce qui est attendu et un peu casse-pieds…

 

Génération de l’EDMX avant ajout des contraintes

 

Voici le script de création de la table :

CREATE TABLE [dbo].[MATABLE](
	[PK] [int] IDENTITY(1,1) NOT NULL,
	[COLONNE1] [varchar](50) NULL,
	[COLONNE2] [varchar](50) NULL,
	[COLONNE3] [varchar](50) NULL,
	[CREATION] [datetime] NULL,
 CONSTRAINT [PK_MATABLE] PRIMARY KEY CLUSTERED 
(
	[PK] ASC
))

GO

Ici, on génère l’EDMX. Les propriétés sur le champ CREATION sont ainsi :
ADONET-EdmxNullable

Maintenant, si on joue ceci :

ALTER TABLE [dbo].[MATABLE]
	ALTER COLUMN [CREATION] [datetime] NOT NULL
GO
ALTER TABLE [dbo].[MATABLE]
       ADD  CONSTRAINT [DF_MATABLE_CREATION]  DEFAULT (getdate()) FOR [CREATION]
GO

Côté EF, il ne va rien voir, mais ce sera un gros drâme :

Insertion EF avec Nullable

Pourquoi ?
Simplement parce que EF va générer la requête suivante :

exec sp_executesql N'
	insert [dbo].[MATABLE]([COLONNE1], [COLONNE2], [COLONNE3], [CREATION])
	values (@0, @1, @2, null)
	select [PK]
	from [dbo].[MATABLE]
	where @@ROWCOUNT > 0 and [PK] = scope_identity()',
N'@0 varchar(50),@1 varchar(50),@2 varchar(50)',
@0='Colonne#1',
@1='Colonne#2',
@2='Colonne#3'

Il est plus visible, là, le problème ?
EF va inclure TOUTES les colonnes dans son insert et forcer tous les champ non spécifiés à NULL, ce qui va exclure l’usage des valeurs par défaut.

Si on actualise l’EDMX avec l’assistant « Mettre à jour le modèle à partie de la base de données… » puis onglet « Actualiser », les propriétés du champ CREATION ne vont pas changer. Pour que la modification soit réellement prise en compte, il faut supprimer la table dans le modèle pour ensuite l’ajouter…

 

Solutions

 

 

Solution #1

La première solution consiste à ajouter la date directement dans l’EDMX.
On ne peut pas ajouter « GETDATE() » ni « DateTime.Now » car cela génère une erreur à la compilation :
« Erreur 54 : La valeur par défaut (GETDATE()) n’est pas valide pour DateTime. La valeur doit avoir la forme « yyyy-MM-dd HH:mm:ss.fffZ ». »

Donc, il faut faire ça :
Valeur par défaut en dur

Le SQL généré sera donc :

exec sp_executesql N'
	insert [dbo].[MATABLE]([COLONNE1], [COLONNE2], [COLONNE3], [CREATION])
	values (@0, @1, @2, @3)
	select [PK]
	from [dbo].[MATABLE]
	where @@ROWCOUNT > 0 and [PK] = scope_identity()',
N'@0 varchar(50),@1 varchar(50),@2 varchar(50),@3 datetime',
@0='Colonne#1',
@1='Colonne#2',
@2='Colonne#3',
@3='juil 17 2014  3:30:00:000PM'

Dans le designer, on aura :

        [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=false)]
        [DataMemberAttribute()]
        public global::System.DateTime CREATION
        {
            get
            {
                return _CREATION;
            }
            set
            {
                OnCREATIONChanging(value);
                ReportPropertyChanging("CREATION");
                _CREATION = StructuralObject.SetValidValue(value);
                ReportPropertyChanged("CREATION");
                OnCREATIONChanged();
            }
        }
        private global::System.DateTime _CREATION = new DateTime(635412078000000000, DateTimeKind.Unspecified);
        partial void OnCREATIONChanging(global::System.DateTime value);
        partial void OnCREATIONChanged();

Je sais pas vous, mais moi, ça me chiffonne quand même pas mal.
Déjà parce que c’est moche, ensuite parce que y a qu’une seule date possible… (et dans le cadre d’un champ d’audit, par exemple, ça sert vraiment à rien).

Dans les moments les plus sombres de la nuit, on peut aussi, après avoir beaucoup bu, modifier directement le designer pour avoir ça :

private global::System.DateTime _CREATION = DateTime.Now;

Il va sans dire que même si c’est une solution valide et qui fonctionne, il ne faut pas oublier que le fichier est auto-généré et porte l’avertissement : « Les modifications manuelles apportées à ce fichier sont remplacées si le code est régénéré. ».

 

Solution #2

Alimenter le champ à chaque fois que l’on créé l’objet.
C’est à dire :

            using (var context = new SAMPLESEntities())
            {
                var ligne = new MATABLE
                                {
                                    COLONNE1 = "Colonne#1",
                                    COLONNE2 = "Colonne#2",
                                    COLONNE3 = "Colonne#3",
                                    CREATION = DateTime.Now
                                };
                context.MATABLE.AddObject(ligne);
                context.SaveChanges();
            }

Là aussi, la valeur par défaut perd quand même bien de son intérêt…
De plus, dans le cas où c’est fonctionnellement autre chose (par exemple un champ texte d’audit avec le nom de l’applicatif), faudra changer toutes les occurrences si la valeur doit évoluer (bon, j’espère que là, la valeur ne sera pas partout en dur, mais qu’une constante [RESX ?] sera utilisée…).

 

Solution #3

Le coup de la classe partielle :

    public partial class MATABLE
    {
        public MATABLE()
        {
            CREATION = DateTime.Now;
        }
    }

A priori, la meilleure solution.
Elle n’est pas réellement satisfaisante (la date correspond au moment de la création de l’objet, pas de l’insertion en base de données), mais c’est celle qui permet de centraliser le plus la résolution du problème.

 

Qu’en est-il sur des versions plus récentes de Visual Studio ?

 

Bonne question, merci de l’avoir posée.

Dans Visual Studio 2013, si on génère un EDMX de la même manière, le résultat sera assez différent :
Au lieu d’avoir l’EDMX et son designer, on aura ceci :
EDMX par VS2013

Comme ça, ça a l’air beaucoup plus complexe, mais c’est surtout beaucoup mieux.

Voici la tête de la classe MATABLE :

    using System;
    using System.Collections.Generic;
    
    public partial class MATABLE
    {
        public int PK { get; set; }
        public string COLONNE1 { get; set; }
        public string COLONNE2 { get; set; }
        public string COLONNE3 { get; set; }
        public System.DateTime CREATION { get; set; }
    }

Y a pas à dire, c’est quand même bien plus proche de Code First !
Cependant, en pratique, les problèmes expliqués plus haut sont les mêmes.
L’IHM est flat, plus jolie (à mon sens, à mi chemin entre VS2010 et 2012), mais on a toujours :
Ma table version 2013

Du coup, la classe partielle est toujours d’actualité !

Catégories :.Net, C#, Développement
  1. Aucun commentaire pour l’instant.
  1. 18/07/2014 à 12:29
  2. 25/09/2014 à 10:03

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

%d blogueurs aiment cette page :