SQL-CLR-Typenkonflikte (LINQ to SQL)
Aktualisiert: November 2007
LINQ to SQL automatisiert einen Großteil des Übersetzungsprozesses zwischen dem Objektmodell und SQL Server. Trotzdem verhindern einige Situationen die genaue Übersetzung. In den folgenden Abschnitten werden diese wichtigen Konflikte zwischen den CLR (Common Language Runtime)-Typen und den SQL Server-Datenbanktypen zusammengefasst. Ausführliche Informationen zu bestimmten Typmappings und Funktionsübersetzungen finden Sie unter SQL CLR-Typzuordnung (LINQ to SQL) und Datentypen und Funktionen (LINQ to SQL).
Datentypen
Eine Übersetzung zwischen CLR und SQL Server wird ausgeführt, wenn eine Abfrage an die Datenbank gesendet und die Ergebnisse an das Objektmodell zurückgesendet werden. Zum Beispiel erfordert folgende Transact-SQL-Abfrage zwei Wertekonvertierungen:
Select DateOfBirth From Customer Where CustomerId = @id
Damit die Abfrage von SQL Server ausgeführt werden kann, muss der Wert für den Transact-SQL-Parameter angegeben werden. In diesem Beispiel muss der id-Parameterwert zunächst von einem CLR-System.Int32-Typ in einen SQL Server-INT-Typ übersetzt werden, damit die Datenbank den Wert interpretieren kann. Um anschließend die Ergebnisse abzurufen, muss die DateOfBirth-Spalte von SQL Server von einem SQL Server-DATETIME-Typ in einen CLR-System.DateTime-Typ übersetzt werden, damit er im Objektmodell verwendet werden kann. In diesem Beispiel haben die Typen im CLR-Objektmodell und der SQL Server-Datenbank natürliche Mappings. Dies ist jedoch nicht immer der Fall.
Fehlende Gegenstücke
Die folgenden Typen weisen keine angemessenen Gegenstücke auf.
Konflikte im CLR-System-Namespace:
Ganzzahlen ohne Vorzeichen. Diese Typen werden in der Regel größeren Äquivalenten mit Vorzeichen zugeordnet, um ein Überlaufen zu vermeiden. Literale können auf der Basis des Werts in eine mit Vorzeichen versehene Zahl der gleichen oder einer kleineren Größe umgewandelt werden.
Boolean. Diese Typen können einem Bit oder einem größeren numerischen Wert/einer Zeichenfolge zugeordnet werden. Ein Literal kann einem Ausdruck zugeordnet werden, der den gleichen Wert ergibt (Beispiel: 1=1 in SQL für True in CLS).
TimeSpan. Dieser Typ steht für den Unterschied zwischen zwei DateTime-Werten und entspricht nicht dem timestamp von SQL Server. Die CLR-Timespan wird möglicherweise auch dem TIME-Typ in SQL Server zugeordnet. Der TIME-Typ in SQL Server kann nur positive Werte unter 24 Stunden darstellen. Die CLR-Timespan hat einen viel größeren Bereich.
Hinweis: SQL Server-spezifische .NET Framework-Typen in System.Data.SqlTypes sind in diesem Vergleich nicht berücksichtigt.
Konflikte in SQL Server:
Typen mit Zeichen fester Länge. Transact-SQL unterscheidet zwischen Unicode- und anderen Kategorien und verfügt je Kategorie über drei separate Typen: nchar/char mit fester Länge, nvarchar/varchar mit variabler Länge und ntext/text mit größerer Kapazität. Typen mit Zeichen fester Länge könnten zum Abrufen von Zeichen dem CLR-System.Char-Typ zugeordnet werden. Hinsichtlich Konvertierung und Verhalten entsprechen sie jedoch nicht dem gleichen Typ.
Bit. Obwohl die bit-Domäne die gleiche Anzahl von Werten aufweist wie Nullable<Boolean>, handelt es sich um verschiedene Typen. Bit nimmt die Werte 1 und 0 anstelle von true/false an und kann nicht als Äquivalent zu booleschen Ausdrücken verwendet werden.
Timestamp. Im Gegensatz zum CLR-Timespan-Typ stellt der SQL Server-TIMESTAMP-Typ eine von der Datenbank erzeugte Zahl mit 8 Bytes dar, die für jede Aktualisierung eindeutig ist und nicht auf dem Unterschied zwischen DateTime-Werten basiert.
Money und SmallMoney. Diese Typen können Decimal zugewiesen werden, sie sind jedoch grundsätzlich verschieden und werden von serverbasierten Funktionen und Konvertierungen dementsprechend behandelt.
Mehrere Mappings
Einem oder mehreren CLR-Datentypen können viele SQL Server-Datentypen zugeordnet werden. Genauso können einem oder mehreren SQL Server-Typen mehrere CLR-Typen zugeordnet werden. Zwar wird ein Mapping möglicherweise von LINQ to SQL unterstützt, dies bedeutet jedoch nicht, dass die beiden zwischen CLR und SQL Server zugeordneten Typen in Genauigkeit, Bereich und Semantik übereinstimmen. Einige Mappings schließen möglicherweise Abweichungen in einer oder allen Dimensionen ein. Ausführliche Informationen zu diesen potenziellen Unterschieden für die verschiedenen Mappingmöglichkeiten finden Sie unter SQL CLR-Typzuordnung (LINQ to SQL).
Benutzerdefinierte Typen
Benutzerdefinierte CLR-Typen werden für die Überbrückung der Typsystemlücke entworfen. Sie fördern jedoch interessante Aspekte der Typversionierung zu Tage. Eine Versionsänderung auf dem Client kann ggf. nicht durch Änderung des Typs auf dem Datenbankserver zugeordnet werden. Eine solche Änderung führt zu einem weiteren Typkonflikt, bei dem die Typsemantik möglicherweise abweicht und die Versionslücke sichtbar wird. Weitere Komplikationen treten auf, wenn Vererbungshierarchien in aufeinander folgenden Versionen umgestaltet werden.
Ausdruckssemantik
Zusätzlich zum paarweisen Konflikt zwischen CLR- und Datenbanktypen wird die Komplexität des Konflikts durch Ausdrücke erhöht. Konflikte in der Operatorsemantik, der Funktionssemantik, der impliziten Typkonvertierung und in den Rangfolgenregeln müssen berücksichtigt werden.
Die folgenden Unterabschnitte veranschaulichen den Konflikt zwischen anscheinend ähnlichen Ausdrücken. Möglicherweise können SQL-Ausdrücke erzeugt werden, die zu einem angegebenen CLR-Ausdruck semantisch äquivalent sind. Es ist jedoch nicht klar, ob die semantischen Unterschiede zwischen anscheinend ähnlichen Ausdrücken für einen CLR-Benutzer erkennbar sind und ob die für die semantische Gleichheit erforderlichen Änderungen beabsichtigt sind oder nicht. Dies ist insbesondere dann ein Problem, wenn ein Ausdruck für einen Satz von Werten ausgewertet wird. Die Sichtbarkeit der Unterschiede kann je nach den Daten variieren und lässt sich beim Codieren und Debuggen nur schwer identifizieren.
NULL-Semantik
SQL-Ausdrücke stellen eine Logik mit drei Werten für boolesche Ausdrücke bereit. Das Ergebnis kann den Wert true, false oder NULL haben. Im Gegensatz dazu gibt CLR ein boolesches Ergebnis mit zwei Werten für Vergleiche an, die NULL-Werte einschließen. Betrachten Sie folgenden Code:
Dim i? As Integer = Nothing
Dim j? As Integer = Nothing
If i = j Then
' This branch is executed.
End If
Nullable<int> i = null;
Nullable<int> j = null;
if (i == j)
{
// This branch is executed.
}
-- 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'.)
Ein ähnliches Problem tritt bei Annahme von Ergebnissen mit zwei Werten auf.
If (i = j) Or (i <> j) Then ' Redundant condition.
' ...
End If
if ((i == j) || (i != j)) // Redundant condition.
{
// ...
}
-- 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.
Im vorherigen Fall erhalten Sie bei der SQL-Erzeugung ein gleichwertiges Verhalten, die Übersetzung kann jedoch Ihre Absicht möglicherweise nicht korrekt widerspiegeln.
LINQ to SQL erzwingt für SQL keine null-Vergleichssemantik von C# oder Visual Basic nothing-Vergleichssemantik. Vergleichsoperatoren werden syntaktisch zu ihren SQL-Entsprechungen übersetzt. Die Semantik reflektiert SQL-Semantik, wie von den Server- oder Verbindungseinstellungen definiert. Die beiden NULL-Werte gelten bei den standardmäßigen SQL Server-Einstellungen als ungleich (obwohl Sie die Semantik über die Einstellungen ändern können). Dennoch berücksichtigt LINQ to SQL keine Servereinstellungen in der Abfrageübersetzung.
Ein Vergleich mit dem null-Literal (nothing-Literal) wird zur entsprechenden SQL-Version (is null oder is not null) übersetzt.
Der Wert von null (nothing) in der Zusammenstellung wird von SQL Server definiert. LINQ to SQL ändert die Zusammenstellung nicht.
Typkonvertierung und Heraufstufung
SQL unterstützt einen umfangreichen Satz impliziter Konvertierungen in Ausdrücken. Ähnliche Ausdrücke in C# würden eine explizite Umwandlung erfordern. Beispiel:
Der Nvarchar-Typ und der DateTime-Typ können in SQL ohne explizite Umwandlungen verglichen werden. C# erfordert explizite Konvertierung.
Decimal wird in SQL implizit in DateTime konvertiert. C# ermöglicht keine implizite Konvertierung.
In gleicher Weise unterscheidet sich die Typreihenfolge in Transact-SQL von derjenigen in C#, da der zu Grunde liegende Typsatz abweicht. In der Tat gibt es keine klare Teilmenge-Obermenge-Beziehung zwischen den Rangfolgenlisten. Beispielsweise führt der Vergleich von nvarchar mit varchar zur impliziten Konvertierung des varchar-Ausdrucks in nvarchar. CLR bietet keine äquivalente Heraufstufung.
In einfachen Fällen führen diese Unterschiede zu CLR-Ausdrücken mit Umwandlungen, die für den entsprechenden SQL-Ausdruck redundant sind. Noch wichtiger ist, dass die direkten Ergebnisse eines SQL-Ausdrucks implizit auf einen Typ heraufgestuft werden können, der kein genaues Gegenstück in C# aufweist (und umgekehrt). Insgesamt führen Tests, Debuggen und Validierung solcher Ausdrücke zu einer erheblichen Belastung für die Benutzer.
Sortierreihenfolge
Transact-SQL unterstützt explizite Sortierungen als Anmerkungen zu Zeichenfolgentypen. Diese Sortierreihenfolgen bestimmen die Gültigkeit von bestimmten Vergleichen. Zwei Spalten mit anderen expliziten Sortierreihenfolgen zu vergleichen ist z. B. unzulässig. Die Verwendung eines erheblich einfacheren CTS-Zeichenfolgentyps verursacht keine solchen Fehler. Betrachten Sie das folgende Beispiel:
create table T2 (
Col1 nvarchar(10),
Col2 nvarchar(10) collate Latin_general_ci_as
)
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
class C
{
string s1; // Map to T2.Col1.
string s2; // Map to T2.Col2.
void Compare()
{
if (s1 == s2) // This is correct.
{
// ...
}
}
}
Select …
From …
Where Col1 = Col2
-- Error, collation conflict.
Praktisch erstellt die Sortierreihenfolge-Unterklausel einen beschränkten Typ, der nicht ersetzbar ist.
Auf ähnliche Weise kann die Sortierreihenfolge über Typsysteme hinweg deutlich abweichen. Dieser Unterschied wirkt sich auf die Ergebnissortierung aus. Guid wird über alle 16 Bytes hinweg in lexikografischer Reihenfolge (IComparable()), sortiert. T-SQL hingegen vergleicht GUIDs in folgender Reihenfolge: node(10-15), clock-seq(8-9), time-high(6-7), time-mid(4-5), time-low(0-3). Diese Sortierung wurde in SQL 7.0 erstellt, als von NT erstellte GUIDs eine entsprechende Oktettreihenfolge hatten. Dieser Ansatz stellte sicher, dass auf dem gleichen Node-Cluster erzeugte GUIDs nach Timestamp in sequenzieller Reihenfolge zusammengestellt wurden. Der Ansatz war auch für die Erstellung von Indizes nützlich (inserts werden nicht zu zufälligen E/As, sondern zu appends). Die Reihenfolge wurde aufgrund von Datenschutzaspekten später in Windows durcheinander gebracht, SQL muss jedoch kompatibel bleiben. Eine Problemumgehung besteht in der Verwendung von SqlGuid anstelle von Guid.
Unterschiede zwischen Operatoren und Funktionen
Im Wesentlichen vergleichbare Operatoren und Funktionen verfügen über eine leicht andere Semantik. Beispiel:
C# gibt auf der Grundlage der lexikalischen Reihenfolge von Operanden für logische Operatoren && und || als Kurzschlusssemantik an. SQL wurde andererseits für satzbasierte Abfragen definiert und bietet daher mehr Optimierungsfreiheit hinsichtlich der Ausführungsreihenfolge. Einige der Auswirkungen schließen Folgendes ein:
Eine semantisch angemessene Übersetzung erfordert in SQL das Konstrukt "CASE … WHEN … THEN", um die Neuanordnung der Operandenausführung zu vermeiden.
Eine freie Übersetzung in die Operatoren AND/OR kann unerwartete Fehler verursachen, wenn der C#-Ausdruck von der Auswertung des zweiten Operanden abhängig ist, die sich auf das Ergebnis der Auswertung des ersten Operanden stützt.
Die Round()-Funktion weist in .NET Framework und in T-SQL unterschiedliche Semantiken auf.
Der Startindex für Zeichenfolgen in CLR ist 0, in SQL jedoch 1. Aus diesem Grund muss jede Funktion mit einem Index übersetzt werden.
CLR unterstützt den Modulusoperator ("%") für Gleitkommazahlen, SQL jedoch nicht.
Der Like-Operator ruft automatische Überladungen auf der Grundlage von impliziten Konvertierungen ab. Obwohl der Like-Operator für Zeichenfolgen mit Zeichen definiert ist, ermöglicht die implizite Konvertierung von numerischen Typen oder DateTime-Typen die Verwendung dieser abweichenden Typen auch mit Like. In CTS sind vergleichbare implizite Konvertierungen nicht vorhanden. Deshalb werden zusätzliche Überladungen benötigt.
Hinweis: Dieses Verhalten des Like-Operators gilt nur für C#, das Like-Schlüsselwort in Visual Basic ist unverändert.
Der Überlauf wird in SQL stets geprüft, muss jedoch in C# (nicht jedoch in Visual Basic) explizit angegeben werden, um Umbrüche zu vermeiden. Gegeben sind die Ganzzahlspalten C1, C2 und C3, wenn C1+C2 in C3 gespeichert wird (Aktualisierung von T-Satz 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.
' 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
// 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.
SQL führt eine symmetrische arithmetische Rundung aus, während .NET Framework unverzerrte Rundung (Banker's Rounding) verwendet. Weitere Informationen finden Sie im Knowledge Base-Artikel 196652.
Standardmäßig wird in SQL die Groß- und Kleinschreibung bei Zeichenfolgenvergleichen für allgemeine Gebietsschemas nicht beachtet. In Visual Basic und C# wird die Groß-/Kleinschreibung beachtet. Beispiel: s == "Food" (s = "Food" in Visual Basic) und s == "Food" können zu verschiedenen Ergebnissen führen, wenn sfood lautet.
-- 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.
' 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.
// 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.
Operatoren/Funktionen, die in SQL auf Argumente des Typs mit Zeichen fester Länge angewendet werden, weisen eine deutlich andere Semantik auf als die gleichen Operatoren/Funktionen, die auf eine CLR-System.String angewendet werden. Dies kann auch als Erweiterung der Problematik fehlender Gegenstücke angesehen werden, die in den Abschnitten zu Typen behandelt wird.
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. 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".
// 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"
Ein ähnliches Problem tritt bei der Zeichenfolgenverkettung auf.
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".
Zusammenfassend kann eine Konvolutübersetzung für CLR-Ausdrücke erforderlich sein. Es können auch weitere Operatoren/Funktionen benötigt werden, um SQL-Funktionen zur Verfügung zu stellen.
Typumwandlung
In C# und SQL können Benutzer die Standardsemantik von Ausdrücken überschreiben, indem sie explizite Typen (Cast und Convert) verwenden. Die Nutzung dieser Möglichkeit über die Grenzen des Typsystems hinweg führt jedoch zu einem Dilemma. Eine SQL-Umwandlung, die die gewünschte Semantik bereitstellt, kann nicht einfach in eine entsprechende C#-Umwandlung übersetzt werden. Andererseits kann eine C#-Umwandlung nicht direkt in eine äquivalente SQL-Umwandlung übersetzt werden, da es zu Typkonflikten, fehlenden Gegenstücken und unterschiedlichen Typhierarchien kommt. Es gibt einen Kompromiss zwischen der Darstellung des Typsystemkonflikts und dem Verlust eines großen Teils der Ausdrucksfunktionalität.
In anderen Fällen ist die Typumwandlung möglicherweise nicht in beiden Domänen für die Validierung eines Ausdrucks erforderlich. Sie kann jedoch u. U. notwendig sein, um sicherzustellen, dass ein vom Standard abweichendes Mapping korrekt auf den Ausdruck angewendet wird.
-- 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
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
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
}
}
}
Select *
From T5
Where Col1 + Col2 > 4
-- "Col1 + Col2" expr evaluates to '32'
Leistungsaspekte
Das Berücksichtigen verschiedener Typunterschiede zwischen SQL Server und CLR führt bei Schnittstellen zwischen den Typsystemen von CLR und SQL Server möglicherweise zu Leistungsbeeinträchtigungen. Im Folgenden finden Sie Beispielszenarien, die sich auf die Leistung auswirken:
Erzwungene Reihenfolge der Evaluierung für logische AND/OR-Operatoren
Das Generieren von SQL, um durchzusetzen, dass die Reihenfolge der Prädikatevaluierung die Fähigkeit des SQL-Optimierungsprogramms einschränkt.
Typkonvertierungen durch einen CLR-Compiler oder durch eine objektrelationale Abfrageimplementierung können zu Problemen bei der Indexverwendung führen.
Beispiel:
-- Table DDL create table T5 ( Col1 varchar(100) )
Class C5 Dim s As String ' Map to T5.Col1. End Class
class C5 { string s; // Map to T5.Col1. }
Berücksichtigen Sie die Übersetzung des Ausdrucks (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.
Zusätzlich zu Semantikunterschieden müssen bei Schnittstellen der Typsysteme von SQL Server und CLR Auswirkungen auf die Leistung berücksichtigt werden. Bei großen Datensätzen können solche Leistungsprobleme aufzeigen, ob eine Anwendung zur Bereitstellung geeignet ist.