Incompatibilité entre types SQL-CLR

LINQ to SQL automatise une grande partie de la traduction entre le modèle objet et SQL Server. Néanmoins, certaines situations empêchent une traduction exacte. Ces incompatibilités clés entre les types CLR (Common Language Runtime) et les types de base de données SQL Server sont résumés dans les sections suivantes. Vous trouverez plus d’informations sur les mappages de types et la traduction de fonctions spécifiques dans SQL-CLR Mappage de types et types de données et fonctions.

Types de données

La traduction entre le CLR et SQL Server se produit lorsqu’une requête est envoyée à la base de données et lorsque les résultats sont renvoyés à votre modèle objet. Par exemple, la requête Transact-SQL suivante nécessite deux conversions de valeurs :

Select DateOfBirth From Customer Where CustomerId = @id

Avant que la requête puisse être exécutée sur SQL Server, la valeur du paramètre Transact-SQL doit être spécifiée. Dans cet exemple, la valeur du id paramètre doit d’abord être traduite d’un type CLR System.Int32 en type SQL Server INT afin que la base de données puisse comprendre la valeur. Ensuite, pour récupérer les résultats, la colonne SQL Server DateOfBirth doit être traduite d’un type SQL Server DATETIME vers un type CLR System.DateTime à utiliser dans le modèle objet. Dans cet exemple, les types du modèle objet CLR et de la base de données SQL Server ont des mappages naturels. Mais ce n’est pas toujours le cas.

Contreparties manquantes

Les types suivants n’ont pas d’équivalents raisonnables.

  • Incompatibilités dans l’espace de noms CLR System :

    • Entiers non signés. Ces types sont généralement mappés à leurs équivalents signés de plus grande taille pour éviter le dépassement de capacité. Les littéraux peuvent être convertis en numérique signé de taille équivalente ou inférieure, selon la valeur.

    • Boolean. Ces types peuvent être mappés à un bit, à un nombre plus grand ou à une chaîne. Un littéral peut être mappé à une expression qui prend la même valeur (par exemple, 1=1 dans SQL pour True clS).

    • TimeSpan. Ce type représente la différence entre deux DateTime valeurs et ne correspond pas à SQL timestamp Server. Le CLR System.TimeSpan peut également mapper au type SQL Server TIME dans certains cas. Le type SQL Server TIME était destiné uniquement à représenter des valeurs positives inférieures à 24 heures. Le CLR TimeSpan a une gamme beaucoup plus grande.

    Remarque

    Les types System.Data.SqlTypes .NET Framework spécifiques à SQL Server ne sont pas inclus dans cette comparaison.

  • Incompatibilités dans SQL Server :

    • Types de caractères de longueur fixe. Transact-SQL fait la distinction entre les catégories Unicode et non Unicode et a trois types distincts dans chaque catégorie : longueur fixe, longueur nchar/charnvarchar/varcharvariable et taille ntext/textsupérieure. Les types de caractères de longueur fixe peuvent être mappés au type CLR System.Char pour récupérer des caractères, mais ils ne correspondent pas vraiment au même type dans les conversions et le comportement.

    • Bit. Bien que le bit domaine ait le même nombre de valeurs que Nullable<Boolean>, les deux sont des types différents. Bitprend des valeurs 1 et 0 au lieu detrue/false , et ne peut pas être utilisé comme équivalent aux expressions booléennes.

    • Horodatage. Contrairement au type CLR System.TimeSpan , le type SQL Server TIMESTAMP représente un nombre de 8 octets généré par la base de données unique pour chaque mise à jour et n’est pas basé sur la différence entre DateTime les valeurs.

    • Money et SmallMoney. Ces types peuvent être mappés à Decimal, mais ce sont essentiellement des types différents et sont traités comme tels par les fonctions et conversions basées sur le serveur.

Mappages multiples

Il existe de nombreux types de données SQL Server que vous pouvez mapper à un ou plusieurs types de données CLR. Il existe également de nombreux types CLR que vous pouvez mapper à un ou plusieurs types SQL Server. Même si un mappage est pris en charge par LINQ to SQL, cela n'implique pas nécessairement que deux types mappés entre CLR et SQL Server présenteront une précision, une plage et une sémantique identiques. Certains mappages peuvent inclure des différences dans l’une ou l’autre de ces dimensions. Vous trouverez des détails sur ces différences potentielles pour les différentes possibilités de mappage à SQL-CLR Mappage de types.

Types définis par l’utilisateur

Les types CLR définis par l'utilisateur sont conçus pour aider à combler le fossé entre les systèmes de types. Néanmoins, ils soulèvent des problèmes intéressants sur le versionnage de type. Une modification de la version sur le client peut ne pas être mise en correspondance par une modification du type stocké sur le serveur de base de données. Toute modification de ce type entraîne une autre incompatibilité de type où la sémantique de type peut ne pas correspondre et l’écart de version est susceptible de devenir visible. Des complications supplémentaires se produisent lorsque les hiérarchies d’héritage sont réorganisées au fil des versions.

Sémantique d’expression

En plus de l’incompatibilité entre les types CLR et de base de données, les expressions ajoutent de la complexité à l’incompatibilité. Les incompatibilités dans la sémantique des opérateurs, la sémantique de fonction, la conversion de type implicite et les règles de précédence doivent être prises en compte.

Les sous-sections suivantes illustrent l’incompatibilité entre des expressions apparemment similaires. Il peut être possible de générer des expressions SQL qui sont sémantiquement équivalentes à une expression CLR donnée. Toutefois, il n’est pas clair si les différences sémantiques entre les expressions apparemment similaires sont évidentes pour un utilisateur CLR et, par conséquent, si les modifications requises pour l’équivalence sémantique sont prévues ou non. Il s’agit d’un problème particulièrement critique lorsqu’une expression est évaluée pour un ensemble de valeurs. La visibilité de la différence peut dépendre des données et être difficile à identifier pendant le codage et le débogage.

Sémantique Nulle

Les expressions SQL fournissent une logique à trois valeurs pour les expressions booléennes. Le résultat peut être vrai, false ou null. En revanche, CLR spécifie le résultat booléen à deux valeurs pour les comparaisons impliquant des valeurs Null. Considérez le code suivant :

Nullable<int> i = null;
Nullable<int> j = null;
if (i == j)
{
    // This branch is executed.
}
Dim i? As Integer = Nothing
Dim j? As Integer = Nothing
If i = j Then
    '  This branch is executed.
End If
-- Assume col1 and col2 are integer columns with null values.
-- Assume that ANSI null behavior has not been explicitly
--  turned off.
Select …
From …
Where col1 = col2
-- Evaluates to null, not true and the corresponding row is not
--   selected.
-- To obtain matching behavior (i -> col1, j -> col2) change
--   the query to the following:
Select …
From …
Where
    col1 = col2
or (col1 is null and col2 is null)
-- (Visual Basic 'Nothing'.)

Un problème similaire se produit avec l’hypothèse concernant les résultats à deux valeurs.

if ((i == j) || (i != j)) // Redundant condition.
{
    // ...
}
If (i = j) Or (i <> j) Then ' Redundant condition.
    ' ...
End If
-- Assume col1 and col2 are nullable columns.
-- Assume that ANSI null behavior has not been explicitly
--   turned off.
Select …
From …
Where
    col1 = col2
or col1 != col2
-- Visual Basic: col1 <> col2.

-- Excludes the case where the boolean expression evaluates
--   to null. Therefore the where clause does not always
--   evaluate to true.

Dans le cas précédent, vous pouvez obtenir un comportement équivalent dans la génération de SQL, mais la traduction peut ne pas refléter précisément votre intention.

LINQ to SQL n’impose pas la sémantique de comparaison C# null ou Visual Basic nothing sur SQL. Les opérateurs de comparaison sont traduits de manière syntactique en leurs équivalents SQL. La sémantique reflète la sémantique SQL telle que définie par les paramètres de serveur ou de connexion. Deux valeurs Null sont considérées comme inégales sous les paramètres SQL Server par défaut (bien que vous puissiez modifier les paramètres pour modifier la sémantique). Indépendamment, LINQ to SQL ne prend pas en compte les paramètres de serveur dans la traduction de requêtes.

Une comparaison avec le littéral null (nothing) est traduite en version SQL appropriée (is null ou is not null).

La valeur (nullnothing) dans le classement est définie par SQL Server ; LINQ to SQL ne modifie pas le classement.

Conversion et promotion de type

SQL prend en charge un ensemble complet de conversions implicites dans les expressions. Des expressions similaires en C# nécessiteraient un cast explicite. Par exemple:

  • Nvarchar et DateTime les types peuvent être comparés dans SQL sans cast explicite ; C# nécessite une conversion explicite.

  • Decimal est implicitement converti DateTime en SQL. C# n’autorise pas une conversion implicite.

De même, la précédence de type dans Transact-SQL diffère de la priorité de type en C#, car l’ensemble sous-jacent de types est différent. En fait, il n’existe aucune relation claire sous-ensemble/superset entre les listes de précédence. Par exemple, la comparaison d’un nvarchar avec une varchar cause la conversion implicite de l’expression varchar en nvarchar. Le CLR ne fournit aucune promotion équivalente.

Dans les cas simples, ces différences soumettent les expressions CLR à des casts redondants pour une expression SQL correspondante. Plus important encore, les résultats intermédiaires d’une expression SQL peuvent être implicitement promus vers un type qui n’a pas d’équivalent précis en C#, et inversement. Dans l’ensemble, les tests, le débogage et la validation de ces expressions ajoutent une charge importante à l’utilisateur.

Classement

Transact-SQL prend en charge des classements explicites tels que les annotations des types de chaînes de caractères. Ces classements déterminent la validité de certaines comparaisons. Par exemple, la comparaison de deux colonnes avec des classements explicites différents est une erreur. L’utilisation d’un type de chaîne CTS beaucoup simplifié n’entraîne pas de telles erreurs. Prenons l’exemple suivant :

create table T2 (
    Col1 nvarchar(10),
    Col2      nvarchar(10) collate Latin_general_ci_as
)
class C
{
string s1;       // Map to T2.Col1.
string s2;       // Map to T2.Col2.

    void Compare()
    {
        if (s1 == s2) // This is correct.
        {
            // ...
        }
    }
}
Class C
    Dim s1 As String    ' Map to T2.Col1.
    Dim s2 As String    ' Map to T2.Col2.
    Sub Compare()
        If s1 = s2 Then ' This is correct.
            ' ...
        End If
    End Sub
End Class
Select …
From …
Where Col1 = Col2
-- Error, collation conflict.

En effet, la sous-clause de classement crée un type restreint qui n’est pas substituable.

De même, l’ordre de tri peut être sensiblement différent entre les systèmes de type. Cette différence affecte le tri des résultats. Guid est trié sur toutes les 16 octets par ordre lexicographique (IComparable()), tandis que T-SQL compare les GUID dans l’ordre suivant : node(10-15), clock-seq(8-9), time-high(6-7), time-mid(4-5), time-low(0-3). Cet ordre a été effectué dans SQL 7.0 lorsque les GUID générés par NT avaient un tel ordre d’octet. L’approche a assuré que les GUID générés au même cluster de nœuds étaient regroupés dans l’ordre séquentiel en fonction de l’horodatage. Elle s'est également avérée utile pour la génération d'index (les insertions deviennent des ajouts plutôt que des E/S aléatoires). L'ordre a été réorganisé plus tard dans Windows pour des raisons de confidentialité, mais SQL doit maintenir la compatibilité. Une solution de contournement consiste à utiliser SqlGuid au lieu de Guid.

Différences d’opérateur et de fonction

Les opérateurs et les fonctions qui sont essentiellement comparables ont une sémantique subtilement différente. Par exemple:

  • C# spécifie la sémantique de court-circuit basée sur l’ordre lexical des opérandes pour les opérateurs && logiques et ||. SQL en revanche est ciblé pour les requêtes basées sur des ensembles et offre donc plus de liberté pour l’optimiseur afin de décider de l’ordre d’exécution. Voici quelques-unes des implications suivantes :

    • La traduction sémantiquement équivalente nécessiterait «CASE ... WHENTHEN" construit dans SQL pour éviter la réorganisation de l’exécution de l’opérande.

    • Une traduction libre vers AND/OR des opérateurs peut entraîner des erreurs inattendues si l’expression C# s’appuie sur l’évaluation du deuxième opérande en fonction du résultat de l’évaluation du premier opérande.

  • Round() la fonction a une sémantique différente dans .NET Framework et dans T-SQL.

  • L’index de départ pour les chaînes est 0 dans le CLR, mais 1 dans SQL. Par conséquent, toute fonction qui possède un index nécessite une traduction de cet index.

  • Le CLR prend en charge l’opérateur modulus ('%') pour les nombres à virgule flottante, mais SQL ne le fait pas.

  • L'opérateur Like acquiert efficacement des surcharges automatiques selon des conversions implicites. Bien que l’opérateur Like soit défini pour fonctionner sur des types de chaînes de caractères, la conversion implicite à partir de types numériques ou DateTime de types permet d’utiliser ces types non-chaînes avec Like tout aussi bien. Dans CTS, les conversions implicites comparables n’existent pas. Par conséquent, des surcharges supplémentaires sont nécessaires.

    Remarque

    Ce Like comportement d’opérateur s’applique uniquement à C# ; le mot clé Visual Basic Like n’est pas modifié.

  • Le dépassement de capacité est toujours vérifié dans SQL mais il doit être spécifié explicitement dans C# (pas dans Visual Basic) pour éviter le bouclage. En fonction des colonnes entières C1, C2 et C3, si C1+C2 est stocké dans C3 (Mettre à jour T Set C3 = C1 + C2).

    create table T3 (
        Col1      integer,
        Col2      integer
    )
    insert into T3 (col1, col2) values (2147483647, 5)
    -- Valid values: max integer value and 5.
    select * from T3 where col1 + col2 < 0
    -- Produces arithmetic overflow error.
    
// C# overflow in absence of explicit checks.
int i = Int32.MaxValue;
int j = 5;
if (i+j < 0) Console.WriteLine("Overflow!");
// This code prints the overflow message.
' Does not apply.
' Visual Basic overflow in absence of implicit check
' (turn off overflow checks in compiler options)
Dim I As Integer = Int32.MaxValue
Dim j As Integer = 5
If I + j < 0 Then
    ' This code prints the overflow message.
    Console.WriteLine("Overflow!")
End If
  • SQL effectue un arrondi arithmétique symétrique lorsque le .NET Framework utilise l’arrondi bancaire. Pour plus d’informations, consultez l’article Base de connaissances 196652.

  • Par défaut, les comparaisons de chaînes de caractères ne sont pas sensibles à la casse dans SQL pour les paramètres régionaux communs. Dans Visual Basic et C#, elles sont sensibles à la casse. Par exemple, s == "Food" (s = "Food" en Visual Basic) et s == "Food" peuvent produire des résultats différents si s est food.

    -- Assume default US-English locale (case insensitive).
    create table T4 (
        Col1      nvarchar (256)
    )
    insert into T4 values ('Food')
    insert into T4 values ('FOOD')
    select * from T4 where Col1 = 'food'
    -- Both the rows are returned because of case-insensitive matching.
    
// C# equivalent on collections of Strings in place of nvarchars.
String[] strings = { "food", "FOOD" };
foreach (String s in strings)
{
    if (s == "food")
    {
        Console.WriteLine(s);
    }
}
// Only "food" is returned.
' Visual Basic equivalent on collections of Strings in place of
' nvarchars.
Dim strings() As String = {"food", "FOOD"}
For Each s As String In strings
    If s = "food" Then
        Console.WriteLine(s)
    End If
Next
' Only "food" is returned.
  • Les opérateurs/fonctions appliquées aux arguments de type caractère de longueur fixe dans SQL ont une sémantique sensiblement différente des mêmes opérateurs/fonctions appliqués au CLR System.String. Cela peut également être considéré comme une extension du problème équivalent manquant abordé dans la section sur les types.

    create table T4 (
        Col1      nchar(4)
    )
    Insert into T5(Col1) values ('21');
    Insert into T5(Col1) values ('1021');
    Select * from T5 where Col1 like '%1'
    -- Only the second row with Col1 = '1021' is returned.
    -- Not the first row!
    
    // Assume Like(String, String) method.
    string s = ""; // map to T4.Col1
    if (System.Data.Linq.SqlClient.SqlMethods.Like(s, "%1"))
    {
        Console.WriteLine(s);
    }
    // Expected to return true for both "21" and "1021"
    
    ' Assume Like(String, String) method.
    Dim s As String    ' Map to T4.Col1.
    If s Like (System.Data.Linq.SqlClient.SqlMethods.Like(s, "%1")) Then
        Console.WriteLine(s)
    End If
    ' Expected to return true for both "21" and "1021".
    

    La concaténation de chaînes pose le même problème.

    create table T6 (
        Col1      nchar(4)
        Col2       nchar(4)
    )
    Insert into T6 values ('a', 'b');
    Select Col1+Col2 from T6
    -- Returns concatenation of padded strings "a   b   " and not "ab".
    

En résumé, une traductionvoluée peut être nécessaire pour les expressions CLR et des opérateurs/fonctions supplémentaires peuvent être nécessaires pour exposer des fonctionnalités SQL.

Cast de type

En C# et dans SQL, les utilisateurs peuvent remplacer la sémantique par défaut des expressions à l’aide de casts de types explicites (Cast et Convert). Toutefois, l’exposition de cette fonctionnalité à travers la limite du système de type pose un dilemme. Un cast SQL qui fournit la sémantique souhaitée ne peut pas être facilement traduit en un cast C# correspondant. En revanche, un cast C# ne peut pas être directement traduit en un cast SQL équivalent en raison d’incompatibilités de type, d’équivalents manquants et de hiérarchies de priorité de type différentes. Il existe un compromis entre l'exposition de l'incompatibilité du système de type et une perte importante de puissance d'expression.

Dans d'autres cas, la conversion de type peut ne pas être nécessaire dans aucun des deux domaines pour la validation d'une expression, mais elle peut être requise pour garantir qu'un mappage autre que le par défaut est correctement appliqué à l'expression.

-- Example from "Non-default Mapping" section extended
create table T5 (
    Col1      nvarchar(10),
    Col2      nvarchar(10)
)
Insert into T5(col1, col2) values ('3', '2');
class C
{
    int x;        // Map to T5.Col1.
    int y;        // Map to T5.Col2.

    void Casting()
    {
        // Intended predicate.
        if (x + y > 4)
        {
            // valid for the data above
        }
    }
}
Class C
    Dim x As Integer        ' Map to T5.Col1.
    Dim y As Integer        ' Map to T5.Col2.

    Sub Casting()
        ' Intended predicate.
        If (x + y) > 4 Then
            ' Valid for the data above.
        End If
    End Sub
End Class
Select *
From T5
Where Col1 + Col2 > 4
-- "Col1 + Col2" expr evaluates to '32'

Problèmes de performance

La prise en compte de certaines différences de types SQL Server-CLR peut entraîner une diminution des performances lors de la transition entre le CLR et les systèmes de types de SQL Server. Voici quelques exemples de scénarios impactant les performances :

  • Ordre d'évaluation de la logique et/ou des opérateurs forcé

  • La génération de SQL pour appliquer l’ordre d’évaluation du prédicat limite la capacité de l’optimiseur SQL.

  • Les conversions de types, introduites par un compilateur CLR ou par une implémentation de requête Object-Relational, peuvent limiter l’utilisation de l’index.

    Par exemple,

    -- Table DDL
    create table T5 (
        Col1      varchar(100)
    )
    
    class C5
    {
        string s;        // Map to T5.Col1.
    }
    
    Class C5
        Dim s As String ' Map to T5.Col1.
    End Class
    

    Considérez la traduction de l’expression (s = SOME_STRING_CONSTANT).

    -- Corresponding part of SQL where clause
    Where …
    Col1 = SOME_STRING_CONSTANT
    -- This expression is of the form <varchar> = <nvarchar>.
    -- Hence SQL introduces a conversion from varchar to nvarchar,
    --   resulting in
    Where …
    Convert(nvarchar(100), Col1) = SOME_STRING_CONSTANT
    -- Cannot use the index for column Col1 for some implementations.
    

En plus des différences sémantiques, il est important de prendre en compte les impacts sur les performances lors de la traversée entre les systèmes de type SQL Server et CLR. Pour les jeux de données volumineux, ces problèmes de performances peuvent déterminer si une application est déployable.

Voir aussi