Kader Yildirim
LINQ à 360 degré, Partie 4
Cet article présente les principales fonctionnalités apportées par LINQ to SQL
Par Kader Yildirim publié le 01/08/2007 à 19:29, lu 3923 fois, 7 pages
 4 | Analyse du code généré par LINQ to SQL
L'assistant a automatiquement crée une classe nommée EntrepriseDataContext qui hérite de la classe DataContext et qui permet d'accéder aux données et de typer le résultat. Par exemple :

ctx.GetTable<Adresse>();

La classe créée par l'assistant est en fait une fine couche d'encapsulation rendant plus concise l'accès aux tables :

public global::System.Data.Linq.Table<Adresse> Adresses {

            get {

                return this.GetTable<Adresse>();

            }

        }

Dans le cas des procédures stockées les choses se gâtent un peu :

[global::System.Data.Linq.StoredProcedure(Name="dbo.LireTypeProjet")]

        public global::System.Collections.Generic.IEnumerable<LireTypeProjet> LireTypeProjet() {

            global::System.Data.Linq.Provider.IQueryResults<LireTypeProjet> result = this.ExecuteMethodCall<LireTypeProjet>(

                this, ((global::System.Reflection.MethodInfo)(global::System.Reflection.MethodInfo.GetCurrentMethod())));

            return ((global::System.Collections.Generic.IEnumerable<LireTypeProjet>)(result));

        }

L'attribut System.Data.Linq.StoredProcedure précise que nous allons travailler avec des procédures stockées. Le reste du code consiste à convertir global::System.Data.Linq.Provider.IQueryResults<LireTypeProjet> en global::System.Collections.Generic.IEnumerable<LireTypeProjet>. Cette étape est très importante car si la méthode retournait le type initial alors à chaque fois qu'une application cliente utilise la valeur de retour une requête serait émise vers la base de données alors qu'avec la conversion on peut utiliser autant de fois que l'on souhaite le retour sans lancer de requêtes supplémentaires. Nous reverrons ce point dans la suite de cet article.
Voici un extrait du code généré :

[global::System.Data.Linq.Table(Name="dbo.Adresse")]

    public partial class Adresse : global::System.Data.Linq.INotifyPropertyChanging,

        global::System.ComponentModel.INotifyPropertyChanged {

L'attribut Table fait le lien entre le monde .Net et SQL. Quant à la propriété Name elle fait correspondre le nom de la table dans la base de données et de la classe C#. Elle n'est utile que si les deux noms sont différents.

De plus on remarque que la classe hérite de global::System.Data.Linq.INotifyPropertyChanging et de global::System.ComponentModel.INotifyPropertyChanged. Cet héritage permet à LINQ to SQL de suivre les changements qui sont opérés sur l'entité. Ainsi lors de la mise à jour de la base de données, le système sait exactement quelles sont les entités modifiées. Toutefois cet héritage est facultatif et dans le cas contraire LINQ to SQL maintient deux instances de l'entité - l'original et la version courante - afin de suivre les changements. Bien que cette solution soit plus gourmande en mémoire elle est plus simple à programmer.
Voici un extrait du code généré dans le cas d'une entité liée à d'autres :

[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]

        public Site() {

            this._PersonnelSites = new global::System.Data.Linq.EntitySet<PersonnelSite>

                                       (new global::System.Action<PersonnelSite>

                                            (this.Attach_PersonnelSites),

                                       new global::System.Action<PersonnelSite>(this.Detach_PersonnelSites));

            this._Adresse = default(global::System.Data.Linq.EntityRef<Adresse>);

        }

La classe EntityRef est utilisée pour le chargement différé. En effet lorsqu'on demande une entité seule celle-ci est chargée et les entités associées ne le sont qu'à la première utilisation. Cette technique permet d'optimiser les temps de chargement initial en évitant de récupérer toutes les tables de la base de données en suivant les relations qui peuvent exister entre elles. Dans le cas d'une relation 1-N, du côté du « 1 » LINQ to SQL utilise la classe EntityRef alors que du côté du « N » il utilise la classe EntitySet. Les relations entre tables seront détaillées un peu plus loin.

[global::System.Data.Linq.Column(Storage="_Code", Name="Code", DBType="NChar(3) NOT NULL",

            IsPrimaryKey=true, CanBeNull=false)]

        public string Code {

            get {

                return this._Code;

            }

            set {

                if ((this._Code != value)) {

                    this.OnPropertyChanging("Code");

                    this._Code = value;

                    this.OnPropertyChanged("Code");

                }

            }

        }

L'attribut Column fait le lien entre le monde .Net et SQL.

La propriété Storage de l'attribut Column pointe sur la donnée membre qui stocke réellement la valeur provenant de la base de données. Ainsi LINQ to SQL affecte ou lit directement la valeur de cette variable (_Code dans notre cas) ce qui permet de court-circuiter le code métier qui aurait pu être ajouté à la propriété (Code dans notre cas).

Quant à la propriété Name elle permet de faire le lien entre le nom de la colonne dans la base de données et la propriété C#.

Enfin la propriété DBType n'est pas utilisée à 100% par LINQ to SQL. En effet que le type SQL soit NChar(3) ou bien NChar(512) il est traduit par le type string de C#.

Si la colonne est de type clé primaire la propriété IsPrimaryKey=true est ajoutée à l'attribut Column. Si elle est de type auto-incrémenté alors LINQ to SQL ajoute la propriété IsDBGenerated=true. Par exemple :

[global::System.Data.Linq.Column(Storage="_PersonnelID", Name="PersonnelID",

            DBType="Int NOT NULL IDENTITY", IsPrimaryKey=true, IsDBGenerated=true, CanBeNull=false)]

        public int PersonnelID {

            get {

                return this._PersonnelID;

            }

        }

Enfin dans le cas d'une colonne ayant un timestamp nous avons le code suivant :

[global::System.Data.Linq.Column(Storage="_Timestamp", Name="Timestamp",

            DBType="rowversion NOT NULL", IsDBGenerated=true, IsVersion=true, CanBeNull=false)]

        public byte[] Timestamp {

            get {

                return this._Timestamp;

            }

        }

La propriété IsVersion=true indique que cette colonne peut être utilisée pour la détection de conflits lors de la mise à jour – optimiste - de la base de données. Dans le cas contraire LINQ to SQL doit comparer les valeurs de toutes les colonnes.
Voici un exemple de code généré par l'assistant :

[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]

        public Site() {

            this._PersonnelSites = new global::System.Data.Linq.EntitySet<PersonnelSite>(

                                       new global::System.Action<PersonnelSite>(this.Attach_PersonnelSites),

                                       new global::System.Action<PersonnelSite>(this.Detach_PersonnelSites));

            this._Adresse = default(global::System.Data.Linq.EntityRef<Adresse>);

        }

On remarque que deux classes sont utilisées : EntityRef et EntitySet. Comme nous l'avons déjà vu la classe EntityRef est utilisée dans le cas d'une relation 1-1 ou bien 1-N pour caractériser la table qui est du côté 1. La classe EntitySet caractérise la table qui est du côté N.

Par exemple la table Site est liée avec un enregistrement de la table Adresse d'où l'utilisation de EntityRef. Par contre, étant donné que l'on a une relation 1-N entre Site et PersonnelSite - pour un enregistrement de type Site on peut avoir plusieurs enregistrements de type PersonnelSite -, l'assistant a généré une classe EntitySet.

Contrairement au constructeur de la classe EntityRef, celui d'EntitySet peut prendre en paramètre deux delegates qui sont appelés lors la création ou suppression d'un lien entre deux enregistrements :

[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]

        private void Attach_PersonnelSites(PersonnelSite entity) {

            this.OnPropertyChanging(null);

            entity.Site = this;

            this.OnPropertyChanged(null);

        }

 

        [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]

        private void Detach_PersonnelSites(PersonnelSite entity) {

            this.OnPropertyChanging(null);

            entity.Site = null;

            this.OnPropertyChanged(null);

        }

Comme la classe EntityRef ne dispose pas d'un tel mécanisme, il faut ajouter un code faisant un travail équivalent dans les propriétés de modification de la relation :

[global::System.Data.Linq.Association(Name="FK_Site_Adresse", Storage="_Adresse",

            OtherKey="AdresseID", ThisKey="AdresseID", IsForeignKey=true)]

        public Adresse Adresse {

            get {

                return this._Adresse.Entity;

            }

            set {

                if ((this._Adresse.Entity != value)) {

                    this.OnPropertyChanging("Adresse");

                    if ((this._Adresse.Entity != null)) {

                        Adresse temp = this._Adresse.Entity;

                        this._Adresse.Entity = null;

                        temp.Sites.Remove(this);

                    }

                    this._Adresse.Entity = value;

                    if ((value != null)) {

                        value.Sites.Add(this);

                    }

                    this.OnPropertyChanged("Adresse");

                }

            }

        }

Au passage on remarquera que pour les propriétés qui participent aux liens avec d'autres tables l'assistant génère un attribut de type Association. Ses propriétés remarquables sont :
  • IsForeignkey précisant qu'Adresse est une référence vers une autre table
  • ThisKey et OtherKey indiquent par quelles propriétés les deux tables sont liées. Si ThisKey n'est pas précisé LINQ to SQL utilise la clé primaire
Pour une procédure stockée ne prenant pas de paramètre et retournant un recordset voici le type de code généré :

[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]

        [global::System.Data.Linq.StoredProcedure(Name="dbo.LireTypeProjet")]

        public global::System.Collections.Generic.IEnumerable<LireTypeProjet> LireTypeProjet() {

            global::System.Data.Linq.Provider.IQueryResults<LireTypeProjet> result =

                this.ExecuteMethodCall<LireTypeProjet>(this,

                ((global::System.Reflection.MethodInfo)(

                global::System.Reflection.MethodInfo.GetCurrentMethod())));

            return ((global::System.Collections.Generic.IEnumerable<LireTypeProjet>)(result));

        }

En ce qui concerne la classe LireTypeProjet elle contient les valeurs retournées par la requête - dans notre cas le code et le nom du projet.

Si la procédure stockée prend des paramètres nous avons :

[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]

        [global::System.Data.Linq.StoredProcedure(Name="dbo.EstUnManager")]

        public int EstUnManager(

            [global::System.Data.Linq.Parameter(Name="@PersonnelID")] global::System.Nullable<int> PersonnelID,

            [global::System.Data.Linq.Parameter(Name="@EstManager")] ref global::System.Nullable<bool> EstManager) {

            global::System.Data.Linq.Provider.IExecuteResults result =

                this.ExecuteMethodCall(this,

                ((global::System.Reflection.MethodInfo)(

                global::System.Reflection.MethodInfo.GetCurrentMethod())), PersonnelID, EstManager);

            EstManager = ((global::System.Nullable<bool>)(result.GetParameterValue(1)));

            return ((int)(result.ReturnValue));

        }

Cette fonction prend en entrée les paramètres IN et retourne les paramètres OUT - un argument de type ref est généré par l'assistant.

Dans le cas des procédures stockées retournant plusieurs recordsets le code généré n'est pas vraiment concluant. En effet l'assistant crée une classe ou il y a seulement les valeurs retournées par le premier recordset. Nous verrons dans la dernière section comment gérer ce cas.
 
» Démarrer une discussion
 
Discussion démarée par Mitsuru Furuta le 08/08/2007 à 21:18, 1 commentaire(s).
Discussion démarée par ButhodS le 24/06/2008 à 10:55, 1 commentaire(s).
Discussion démarée par Matthieu Mezil le 08/08/2007 à 16:56, 1 commentaire(s).
Discussion démarée par Matthieu Mezil le 07/08/2007 à 17:37, 1 commentaire(s).
Discussion démarée par Matthieu Mezil le 06/08/2007 à 12:32, 2 commentaire(s).
Discussion démarée par steftanguy le 13/12/2007 à 23:00, 4 commentaire(s).