SQL-CLR Incompatibilidades de tipo

O LINQ to SQL automatiza grande parte da tradução entre o modelo de objeto e o SQL Server. No entanto, algumas situações impedem a tradução exata. Essas incompatibilidades de chave entre os tipos CLR (Common Language Runtime) e os tipos de banco de dados do SQL Server são resumidas nas seções a seguir. Você pode encontrar mais detalhes sobre mapeamentos de tipo específicos e tradução de funções em SQL-CLR Mapeamento de Tipo e Tipos de Dados e Funções.

Tipos de dados

A tradução entre o CLR e o SQL Server ocorre quando uma consulta está sendo enviada para o banco de dados e quando os resultados são enviados de volta ao seu modelo de objeto. Por exemplo, a seguinte consulta Transact-SQL requer duas conversões de valor:

Select DateOfBirth From Customer Where CustomerId = @id

Antes que a consulta possa ser executada no SQL Server, o valor para o parâmetro Transact-SQL deve ser especificado. Neste exemplo, o valor do id parâmetro deve primeiro ser traduzido de um tipo CLR System.Int32 para um tipo do SQL Server INT para que o banco de dados possa entender qual é o valor. Em seguida, para recuperar os resultados, a coluna do SQL Server DateOfBirth deve ser convertida de um tipo do SQL Server DATETIME para um tipo CLR System.DateTime para uso no modelo de objeto. Neste exemplo, os tipos no modelo de objeto CLR e no banco de dados do SQL Server têm mapeamentos naturais. Mas nem sempre é assim.

Contrapartidas em falta

Os seguintes tipos não têm contrapartidas razoáveis.

  • Incompatibilidades no namespace CLR System :

    • Números inteiros não assinados. Esses tipos geralmente são mapeados para seus equivalentes assinados de tamanho maior para evitar transbordamento. Os literais podem ser convertidos em um número assinado do mesmo tamanho ou de tamanho menor, com base no valor.

    • Booleano. Esses tipos podem ser mapeados para um bit ou para tipos numéricos ou strings maiores. Um literal pode ser mapeado para uma expressão que é avaliada com o mesmo valor (por exemplo, 1=1 em SQL para True em CLS).

    • Duração. Esse tipo representa a diferença entre dois DateTime valores e não corresponde ao timestamp do SQL Server. O CLR System.TimeSpan também pode ser mapeado para o tipo SQL Server TIME em alguns casos. O tipo SQL Server TIME destinava-se apenas a representar valores positivos inferiores a 24 horas. O CLR TimeSpan tem um alcance muito maior.

    Observação

    Os tipos específicos do .NET Framework para SQL Server em System.Data.SqlTypes não estão incluídos nesta comparação.

  • Incompatibilidades no SQL Server:

    • Tipos de caracteres de comprimento fixo. Transact-SQL distingue entre categorias Unicode e não-Unicode e tem três tipos distintos em cada categoria: comprimento nchar/charfixo, comprimento nvarchar/varcharvariável e tamanho ntext/textmaior. Os tipos de caracteres de comprimento fixo podem ser mapeados para o tipo CLR System.Char para recuperar caracteres, mas eles realmente não correspondem ao mesmo tipo em conversões e comportamento.

    • Bit. Embora o bit domínio tenha o mesmo número de valores que Nullable<Boolean>, os dois são tipos diferentes. Bit toma valores 1 e 0 em vez de true/false, e não pode ser usado como um equivalente a expressões booleanas.

    • Carimbo de data/hora. Ao contrário do tipo CLR System.TimeSpan , o tipo SQL Server TIMESTAMP representa um número de 8 bytes gerado pelo banco de dados que é exclusivo para cada atualização e não se baseia na diferença entre DateTime os valores.

    • Money e SmallMoney. Esses tipos podem ser mapeados para Decimal, mas são basicamente tipos diferentes e são tratados como tal por funções e conversões no servidor.

Vários mapeamentos

Há muitos tipos de dados do SQL Server que você pode mapear para um ou mais tipos de dados CLR. Há também muitos tipos de CLR que você pode mapear para um ou mais tipos do SQL Server. Embora um mapeamento possa ser suportado pelo LINQ to SQL, isso não significa que os dois tipos mapeados entre o CLR e o SQL Server sejam uma combinação perfeita em precisão, intervalo e semântica. Alguns mapeamentos podem incluir diferenças em qualquer uma ou em todas essas dimensões. Você pode encontrar detalhes sobre essas diferenças potenciais para as várias possibilidades de mapeamento em SQL-CLR Type Mapping.

Tipos definidos pelo usuário

Os tipos CLR definidos pelo usuário são projetados para ajudar a preencher a lacuna do sistema de tipos. No entanto, surgem questões relevantes sobre o versionamento de tipos. Uma alteração na versão no cliente pode não ser correspondida por uma alteração no tipo armazenado no servidor de banco de dados. Qualquer alteração desse tipo causa outra incompatibilidade de tipo, onde a semântica de tipo pode não corresponder e a lacuna de versão provavelmente se tornará visível. Outras complicações ocorrem à medida que as hierarquias de herança são refatoradas em versões sucessivas.

Semântica de Expressão

Além da incompatibilidade de pares entre CLR e tipos de banco de dados, as expressões adicionam complexidade à incompatibilidade. Incompatibilidades na semântica do operador, semântica de função, conversão de tipo implícita e regras de precedência devem ser consideradas.

As subsecções seguintes ilustram o desfasamento entre expressões aparentemente semelhantes. Pode ser possível gerar expressões SQL que são semanticamente equivalentes a uma determinada expressão CLR. No entanto, não está claro se as diferenças semânticas entre expressões aparentemente semelhantes são evidentes para um usuário CLR e, portanto, se as alterações necessárias para a equivalência semântica são pretendidas ou não. Esta é uma questão especialmente crítica quando uma expressão é avaliada para um conjunto de valores. A visibilidade da diferença pode depender de dados e ser difícil de identificar durante a codificação e depuração.

Semântica Nula

As expressões SQL fornecem uma lógica de três valores para expressões booleanas. O resultado pode ser verdadeiro, falso ou nulo. Por outro lado, o CLR especifica o resultado booleano de dois valores para comparações envolvendo valores nulos. Considere o seguinte código:

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'.)

Um problema semelhante ocorre com a suposição relacionada com resultados binários.

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.

No caso anterior, você pode obter um comportamento equivalente na geração de SQL, mas a tradução pode não refletir com precisão sua intenção.

LINQ to SQL não impõe semântica de comparação C# null ou Visual Basic nothing no SQL. Os operadores de comparação são traduzidos sintaticamente para seus equivalentes SQL. A semântica reflete a semântica SQL conforme definido pelas configurações de servidor ou conexão. Dois valores nulos são considerados desiguais nas configurações padrão do SQL Server (embora você possa alterar as configurações para alterar a semântica). Independentemente disso, o LINQ to SQL não considera as configurações do servidor na tradução de consultas.

Uma comparação com o literal null (nothing) é traduzida para a versão SQL apropriada (is null ou is not null).

O valor de null (nothing) no agrupamento é definido pelo SQL Server; LINQ to SQL não altera o agrupamento.

Conversão de Tipo e Promoção

SQL suporta um rico conjunto de conversões implícitas em expressões. Expressões semelhantes em C# exigiriam um elenco explícito. Por exemplo:

  • Nvarchar e DateTime os tipos podem ser comparados em SQL sem quaisquer casts explícitos; C# requer conversão explícita.

  • Decimal é implicitamente convertido em DateTime SQL. C# não permite uma conversão implícita.

Da mesma forma, a precedência de tipo em Transact-SQL difere da precedência de tipo em C# porque o conjunto subjacente de tipos é diferente. Na verdade, não existe uma relação clara de subconjunto/superconjunto entre as listas de precedências. Por exemplo, comparar um nvarchar com um varchar causa a conversão implícita da varchar expressão em nvarchar. O CLR não oferece promoção equivalente.

Em casos simples, essas diferenças fazem com que expressões CLR com casts sejam redundantes para uma expressão SQL correspondente. Mais importante ainda, os resultados intermediários de uma expressão SQL podem ser implicitamente promovidos para um tipo que não tem contrapartida precisa em C# e vice-versa. No geral, o teste, a depuração e a validação de tais expressões adicionam uma carga significativa ao usuário.

Colação

Transact-SQL suporta agrupamentos explícitos como anotações para tipos de cadeia de caracteres. Estes agrupamentos determinam a validade de certas comparações. Por exemplo, comparar duas colunas com agrupamentos explícitos diferentes é um erro. O uso de um tipo de cadeia de caracteres CTS muito simplificado não causa tais erros. Considere o seguinte exemplo:

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.

Com efeito, a subcláusula de agrupamento cria um tipo restrito que não é substituível.

Da mesma forma, a ordem de classificação pode ser significativamente diferente entre os sistemas de tipo. Essa diferença afeta a classificação dos resultados. Guid é classificado em todos os 16 bytes por ordem lexicográfica (IComparable()), enquanto o T-SQL compara GUIDs na seguinte ordem: node(10-15), clock-seq(8-9), time-high(6-7), time-mid(4-5), time-low(0-3). Essa ordenação foi feita no SQL 7.0 quando GUIDs gerados pelo NT tinham essa ordem de octeto. A abordagem garantiu que os GUIDs gerados no mesmo cluster de nós fossem reunidos em ordem sequencial de acordo com a data e hora. A abordagem também foi útil para a criação de índices (inserções transformam-se em apêndices em vez de IOs aleatórios). A ordem foi alterada posteriormente no Windows devido a preocupações de privacidade, mas deve-se manter a compatibilidade com o SQL. Uma solução alternativa é usar SqlGuid em vez de Guid.

As diferenças entre o operador e a função

Operadores e funções que são essencialmente comparáveis têm semânticas sutilmente diferentes. Por exemplo:

  • C# especifica a semântica de curto-circuito com base na ordem lexical dos operandos para os operadores lógicos && e ||. O SQL, por outro lado, é direcionado para consultas baseadas em conjunto e, portanto, fornece mais liberdade para o otimizador decidir a ordem de execução. Algumas das implicações incluem o seguinte:

    • Uma tradução semanticamente equivalente exigiria "CASE ... WHEN ... THEN" construção em SQL para evitar a reordenação da execução de operandos.

    • Uma tradução livre para operadores AND/OR pode causar erros inesperados se a expressão C# depender da avaliação em que o segundo operando é baseado no resultado da avaliação do primeiro operando.

  • Round() function tem semânticas diferentes no .NET Framework e no T-SQL.

  • O índice inicial para cadeias de caracteres é 0 no CLR, mas 1 no SQL. Portanto, qualquer função que tenha índice precisa de tradução de índice.

  • O CLR suporta o operador de módulo ('%') para números de ponto flutuante, mas o SQL não.

  • O Like operador efetivamente adquire sobrecargas automáticas com base em conversões implícitas. Embora o Like operador seja definido para operar em tipos de cadeia de caracteres, uma conversão implícita de tipos numéricos ou tipos DateTime permite que esses tipos que não sejam de cadeia de caracteres sejam usados com Like da mesma forma. Na STC, não existem conversões implícitas comparáveis. Portanto, sobrecargas adicionais são necessárias.

    Observação

    Esse Like comportamento de operador se aplica somente ao C#, a palavra-chave do Visual Basic Like não é alterada.

  • O estouro é sempre verificado no SQL, mas precisa ser explicitamente especificado em C# (não no Visual Basic) para evitar o wraparound. Dadas as colunas inteiras C1, C2 e C3, se C1+C2 estiver armazenado em C3 (Update 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
  • O SQL executa arredondamento aritmético simétrico, enquanto o .NET Framework usa o arredondamento bancário. Consulte o artigo da Base de conhecimento 196652 para obter detalhes adicionais.

  • Por defeito, nas localizações comuns, as comparações de cadeias de caracteres no SQL não diferenciam entre maiúsculas e minúsculas. Em Visual Basic e em C#, é sensível a maiúsculas e minúsculas. Por exemplo, s == "Food" (s = "Food" no Visual Basic) e s == "Food" podem produzir resultados diferentes se s for 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.
  • Os operadores/funções aplicados a argumentos de tipo de caractere de comprimento fixo em SQL têm semânticas significativamente diferentes dos mesmos operadores/funções aplicados ao CLR System.String. Isso também pode ser visto como uma extensão do problema de contrapartida ausente discutido na seção sobre tipos.

    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".
    

    Um problema semelhante ocorre com a concatenação de cadeias de caracteres.

    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".
    

Em resumo, uma tradução complicada pode ser necessária para expressões CLR e operadores/funções adicionais podem ser necessários para expor a funcionalidade SQL.

Tipo Fundição

Em C# e em SQL, os usuários podem substituir a semântica padrão de expressões usando versões de tipo explícitas (Cast e Convert). No entanto, expor essa capacidade através do limite do sistema de tipos representa um dilema. Um SQL cast que fornece a semântica desejada não pode ser facilmente traduzido para um C# correspondente. Por outro lado, uma conversão em C# não pode ser traduzida diretamente em uma conversão SQL equivalente devido a incompatibilidades de tipo, contrapartes ausentes e hierarquias de precedência de tipo diferentes. Há uma troca entre expor a incompatibilidade do sistema de tipos e sacrificar significativamente o poder de expressão.

Em outros casos, a conversão de tipo pode não ser necessária em nenhum dos domínios para validação de uma expressão, mas pode ser necessária para garantir que um mapeamento não padrão seja aplicado corretamente à expressão.

-- 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'

Problemas de desempenho

A contabilização de certas diferenças de tipos do SQL Server-CLR pode resultar em uma redução de desempenho ao transitar entre os tipos de sistemas CLR e SQL Server. Exemplos de cenários que afetam o desempenho incluem o seguinte:

  • Ordem forçada de avaliação para operadores lógicos e/ou

  • Gerar SQL para impor a ordem de avaliação de predicados restringe a capacidade do otimizador de SQL.

  • As conversões de tipo, sejam elas introduzidas por um compilador CLR ou por uma implementação de consulta Object-Relational, podem reduzir o uso do índice.

    Por exemplo

    -- 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
    

    Considere a tradução da expressão (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.
    

Além das diferenças semânticas, é importante considerar os impactos no desempenho ao cruzar entre os sistemas do tipo SQL Server e CLR. Para grandes conjuntos de dados, esses problemas de desempenho podem determinar se um aplicativo é implantável.

Ver também