Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Si applica a:SQL Server
Quando si codifica la definizione del tipo definito dall'utente (UDT), è necessario implementare varie funzionalità, a seconda che si stia implementando il tipo definito dall'utente come classe o struttura e sulle opzioni di formattazione e serializzazione scelte.
Nell'esempio riportato in questa sezione viene illustrata l'implementazione di un tipo definito dall'utente Point
come struct
(o Structure
in Visual Basic). Il Point
tipo definito dall'utente è costituito da coordinate X e Y implementate come routine delle proprietà.
Quando si definisce un tipo definito dall'utente sono richiesti gli spazi dei nomi seguenti:
using System;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
Lo spazio dei nomi Microsoft.SqlServer.Server
contiene gli oggetti necessari per vari attributi del tipo definito dall'utente e lo spazio dei nomi System.Data.SqlTypes
contiene le classi che rappresentano i tipi di dati nativi di SQL Server disponibili per l'assembly. Potrebbero essere presenti altri spazi dei nomi necessari per il corretto funzionamento dell'assembly. Il tipo definito dall'utente di Point
usa anche lo spazio dei nomi System.Text
per l'uso delle stringhe.
Nota
Gli oggetti di database di Visual C++, ad esempio i tipi definiti dall'utente, compilati con /clr:pure
non sono supportati per l'esecuzione.
Specificare gli attributi
Gli attributi consentono di determinare la modalità di utilizzo della serializzazione per costruire la rappresentazione di archiviazione dei tipi definiti dall'utente e per trasmettere tali tipi al client in base al valore.
Il Microsoft.SqlServer.Server.SqlUserDefinedTypeAttribute
è obbligatorio. L'attributo Serializable
è facoltativo. È anche possibile specificare il Microsoft.SqlServer.Server.SqlFacetAttribute
per fornire informazioni sul tipo restituito di un tipo definito dall'utente. Per altre informazioni, vedere integrazione di CLR : attributi personalizzati per routine CLR.
Attributi del tipo definito dall'utente del punto
Il Microsoft.SqlServer.Server.SqlUserDefinedTypeAttribute
imposta il formato di archiviazione per il tipo definito dall'utente di Point
su Native
.
IsByteOrdered
è impostato su true
, che garantisce che i risultati dei confronti siano gli stessi in SQL Server come se lo stesso confronto fosse stato eseguito nel codice gestito. Il tipo definito dall'utente implementa l'interfaccia System.Data.SqlTypes.INullable
per rendere il tipo definito dall'utente null compatibile.
Il frammento di codice seguente mostra gli attributi per il tipo definito dall'utente Point
.
[Serializable]
[Microsoft.SqlServer.Server.SqlUserDefinedType(Format.Native,
IsByteOrdered=true)]
public struct Point : INullable { ... }
Implementare il supporto dei valori Null
Oltre a specificare gli attributi per gli assembly in modo corretto, il tipo definito dall'utente deve supportare anche i valori Null. I tipi definiti dall'utente caricati in SQL Server sono in grado di riconoscere i valori Null, ma affinché il tipo definito dall'utente riconosca un valore Null, il tipo definito dall'utente deve implementare l'interfaccia System.Data.SqlTypes.INullable
.
È necessario creare una proprietà denominata IsNull
, necessaria per determinare se un valore è Null dall'interno del codice CLR. Quando SQL Server trova un'istanza Null di un tipo definito dall'utente, il tipo definito dall'utente viene salvato in modo permanente usando i normali metodi di gestione dei valori Null. Il server non perde tempo durante la serializzazione o la deserializzazione del tipo definito dall'utente se non è necessario e non sprecare spazio per archiviare un tipo definito dall'utente null. Questo controllo dei valori Null viene eseguito ogni volta che un tipo definito dall'utente viene spostato da CLR, il che significa che l'uso del costrutto di Transact-SQL IS NULL
per verificare la presenza di tipi definiti dall'utente Null deve sempre funzionare. La proprietà IsNull
viene utilizzata anche dal server per verificare se un'istanza è Null. Una volta stabilito che il tipo definito dall'utente è Null, il server è in grado di utilizzare la relativa funzionalità di gestione nativa dei valori Null.
Il metodo get()
di IsNull
non viene fatto in alcun modo con maiuscole e minuscole. Se una variabile Point
@p
è Null
, @p.IsNull
restituirà, per impostazione predefinita, NULL
, non 1
. Questo perché l'attributo SqlMethod(OnNullCall)
del metodo IsNull get()
viene impostato su false. Poiché l'oggetto è Null
, quando viene richiesta la proprietà l'oggetto non viene deserializzato, il metodo non viene chiamato e viene restituito un valore predefinito "NULL".
Esempio
Nell'esempio seguente la variabile is_Null
è privata e mantiene lo stato Null per l'istanza del tipo definito dall'utente. Il codice deve gestire un valore appropriato per is_Null
. Il tipo definito dall'utente deve inoltre avere una proprietà statica denominata Null
che restituisce un'istanza di valore Null del tipo definito dall'utente. In questo modo il tipo definito dall'utente può restituire un valore Null se l'istanza è Null nel database.
private bool is_Null;
public bool IsNull
{
get
{
return (is_Null);
}
}
public static Point Null
{
get
{
Point pt = new Point();
pt.is_Null = true;
return pt;
}
}
Confronto tra IS NULL e IsNull
Si consideri una tabella contenente lo schema Points(id int, location Point)
, dove Point
è un tipo definito dall'utente CLR e le query seguenti:
Query 1:
SELECT ID FROM Points WHERE NOT (location IS NULL); -- Or, WHERE location IS NOT NULL;
Query 2:
SELECT ID FROM Points WHERE location.IsNull = 0;
Entrambe le query restituiscono gli ID di punti con posizioni non Null. Nella Query 1 viene utilizzata la normale gestione dei valori Null e non è necessaria la deserializzazione dei tipi definiti dall'utente. La query 2, invece, deve deserializzare ogni oggetto non Null e chiamare in CLR per ottenere il valore della proprietà IsNull
. Chiaramente, l'uso di IS NULL
presenta prestazioni migliori e non dovrebbe mai esserci un motivo per leggere la proprietà IsNull
di un tipo definito dall'utente dal codice Transact-SQL.
Quindi, qual è l'uso della proprietà IsNull
? In primo luogo, è necessario determinare se un valore è Null dall'interno del codice CLR. In secondo luogo, il server deve verificare se un'istanza di è Null, quindi questa proprietà viene utilizzata dal server. Dopo che è stato determinato come Null, può usare la gestione dei valori Null nativa per gestirla.
Implementare il metodo parse
I metodi Parse
e ToString
consentono le conversioni da e verso le rappresentazioni di stringa del tipo definito dall'utente. Il metodo Parse
consente la conversione di una stringa in un tipo definito dall'utente. Deve essere dichiarato come static
(o Shared
in Visual Basic) e accettare un parametro di tipo System.Data.SqlTypes.SqlString
.
Il codice seguente implementa il metodo Parse
per il tipo definito dall'utente Point
, che separa le coordinate X e Y. Il metodo Parse
ha un singolo argomento di tipo System.Data.SqlTypes.SqlString
e presuppone che i valori X e Y vengano forniti come stringa delimitata da virgole. L'impostazione dell'attributo Microsoft.SqlServer.Server.SqlMethodAttribute.OnNullCall
su false
impedisce che il metodo Parse
venga chiamato da un'istanza Null di Point.
[SqlMethod(OnNullCall = false)]
public static Point Parse(SqlString s)
{
if (s.IsNull)
return Null;
// Parse input string to separate out points.
Point pt = new Point();
string[] xy = s.Value.Split(",".ToCharArray());
pt.X = Int32.Parse(xy[0]);
pt.Y = Int32.Parse(xy[1]);
return pt;
}
Implementare il metodo ToString
Il metodo ToString
converte il tipo definito dall'utente Point
in un valore stringa. In questo caso, la stringa "NULL" viene restituita per un'istanza Null del tipo di Point
. Il metodo ToString
inverte il metodo Parse
utilizzando un System.Text.StringBuilder
per restituire un System.String
delimitato da virgole costituito dai valori delle coordinate X e Y. Poiché per impostazione predefinita InvokeIfReceiverIsNull
false, il controllo di un'istanza Null di Point
non è necessario.
private Int32 _x;
private Int32 _y;
public override string ToString()
{
if (this.IsNull)
return "NULL";
else
{
StringBuilder builder = new StringBuilder();
builder.Append(_x);
builder.Append(",");
builder.Append(_y);
return builder.ToString();
}
}
Esporre le proprietà del tipo definito dall'utente
Il tipo definito dall'utente Point
espone le coordinate X e Y implementate come proprietà pubbliche di lettura/scrittura di tipo System.Int32
.
public Int32 X
{
get
{
return this._x;
}
set
{
_x = value;
}
}
public Int32 Y
{
get
{
return this._y;
}
set
{
_y = value;
}
}
Convalidare i valori definiti dall'utente
Quando si utilizzano dati definiti dall'utente, SQL Server motore di database converte automaticamente i valori binari in valori definiti dall'utente. Ai fini di tale processo di conversione, viene verificato che i valori siano appropriati al formato di serializzazione del tipo e che il valore possa essere deserializzato correttamente. In questo modo, il valore può essere convertito nuovamente in formato binario. Nel caso dei tipi definiti dall'utente ordinati per byte, questo processo assicura anche che il valore binario risultante corrisponda al valore binario originale. In questo modo si impedisce che valori non validi vengano resi persistenti nel database. In alcuni casi, questo livello di controllo potrebbe risultare inadeguato. È possibile che sia necessaria una convalida aggiuntiva quando i valori definiti dall'utente devono trovarsi in un dominio o un intervallo previsto. Un tipo definito dall'utente che implementa, ad esempio, una data potrebbe richiedere che il valore del giorno sia un numero positivo compreso in un determinato intervallo di valori validi.
La proprietà Microsoft.SqlServer.Server.SqlUserDefinedTypeAttribute.ValidationMethodName
del Microsoft.SqlServer.Server.SqlUserDefinedTypeAttribute
consente di specificare il nome di un metodo di convalida eseguito dal server quando i dati vengono assegnati a un tipo definito dall'utente o convertiti in un tipo definito dall'utente. ValidationMethodName
è Null, a indicare che non esiste alcun metodo di convalida.
Esempio
Il frammento di codice seguente mostra la dichiarazione per la classe Point
, che specifica un ValidationMethodName
di ValidatePoint
.
[Serializable]
[Microsoft.SqlServer.Server.SqlUserDefinedType(Format.Native,
IsByteOrdered=true,
ValidationMethodName = "ValidatePoint")]
public struct Point : INullable { ... }
Se si specifica un metodo di convalida, è necessario che includa una firma simile al frammento di codice seguente:
private bool ValidationFunction()
{
if (validation logic here)
{
return true;
}
else
{
return false;
}
}
Il metodo di convalida può avere qualsiasi ambito e deve restituire true
se il valore è valido e false
in caso contrario. Se il metodo restituisce false
o genera un'eccezione, il valore viene considerato non valido e viene generato un errore.
Nell'esempio seguente il codice consente solo valori pari a zero o superiori alle coordinate X e Y.
private bool ValidatePoint()
{
if ((_x >= 0) && (_y >= 0))
{
return true;
}
else
{
return false;
}
}
Limitazioni del metodo di convalida
Il server chiama il metodo di convalida quando il server esegue conversioni, non quando i dati vengono inseriti impostando singole proprietà o quando i dati vengono inseriti utilizzando un'istruzione Transact-SQL INSERT
.
È necessario chiamare in modo esplicito il metodo di convalida dai setter di proprietà e dal metodo Parse
se si desidera che il metodo di convalida venga eseguito in tutte le situazioni. Questo non è un requisito e in alcuni casi potrebbe non essere nemmeno auspicabile.
Esempio di convalida dell'analisi
Per assicurarsi che il metodo ValidatePoint
venga richiamato nella classe Point
, è necessario chiamarlo dal metodo Parse
e dalle routine delle proprietà che impostano i valori delle coordinate X e Y. Il frammento di codice seguente illustra come chiamare il metodo di convalida ValidatePoint
dalla funzione Parse
.
[SqlMethod(OnNullCall = false)]
public static Point Parse(SqlString s)
{
if (s.IsNull)
return Null;
// Parse input string to separate out points.
Point pt = new Point();
string[] xy = s.Value.Split(",".ToCharArray());
pt.X = Int32.Parse(xy[0]);
pt.Y = Int32.Parse(xy[1]);
// Call ValidatePoint to enforce validation
// for string conversions.
if (!pt.ValidatePoint())
throw new ArgumentException("Invalid XY coordinate values.");
return pt;
}
Esempio di convalida delle proprietà
Il frammento di codice seguente illustra come chiamare il metodo di convalida ValidatePoint
dalle routine di proprietà che impostano le coordinate X e Y.
public Int32 X
{
get
{
return this._x;
}
// Call ValidatePoint to ensure valid range of Point values.
set
{
Int32 temp = _x;
_x = value;
if (!ValidatePoint())
{
_x = temp;
throw new ArgumentException("Invalid X coordinate value.");
}
}
}
public Int32 Y
{
get
{
return this._y;
}
set
{
Int32 temp = _y;
_y = value;
if (!ValidatePoint())
{
_y = temp;
throw new ArgumentException("Invalid Y coordinate value.");
}
}
}
Metodi definiti dall'utente del codice
Quando si codificano i metodi UDT, è consigliabile valutare la possibilità che l'algoritmo utilizzato possa cambiare nel tempo. In tal caso, è possibile prendere in considerazione la creazione di una classe separata per i metodi usati dal tipo definito dall'utente. Se l'algoritmo viene modificato, è possibile ricompilare la classe con il nuovo codice e caricare l'assembly in SQL Server senza influire sul tipo definito dall'utente. In molti casi è possibile ricaricare i tipi definiti dall'utente usando l'istruzione Transact-SQL ALTER ASSEMBLY
, ma ciò potrebbe causare problemi con i dati esistenti. Ad esempio, il tipo definito dall'utente Currency
incluso nel database di esempio AdventureWorks2022
usa una funzione ConvertCurrency
per convertire i valori di valuta, implementato in una classe separata. È possibile che gli algoritmi di conversione cambino in modi imprevedibili in futuro o che sia necessaria una nuova funzionalità. La separazione della funzione ConvertCurrency
dall'implementazione del tipo definito dall'utente di Currency
offre maggiore flessibilità durante la pianificazione delle modifiche future.
Esempio
La classe Point
contiene tre semplici metodi per calcolare la distanza: Distance
, DistanceFrom
e DistanceFromXY
. Ogni restituisce un double
calcolando la distanza tra Point
e zero, la distanza da un punto specificato a Point
e la distanza dalle coordinate X e Y specificate a Point
.
Distance
e DistanceFrom
ogni chiamata DistanceFromXY
e illustrano come usare argomenti diversi per ogni metodo.
// Distance from 0 to Point.
[SqlMethod(OnNullCall = false)]
public Double Distance()
{
return DistanceFromXY(0, 0);
}
// Distance from Point to the specified point.
[SqlMethod(OnNullCall = false)]
public Double DistanceFrom(Point pFrom)
{
return DistanceFromXY(pFrom.X, pFrom.Y);
}
// Distance from Point to the specified x and y values.
[SqlMethod(OnNullCall = false)]
public Double DistanceFromXY(Int32 iX, Int32 iY)
{
return Math.Sqrt(Math.Pow(iX - _x, 2.0) + Math.Pow(iY - _y, 2.0));
}
Usare gli attributi SqlMethod
La classe Microsoft.SqlServer.Server.SqlMethodAttribute
fornisce attributi personalizzati che possono essere usati per contrassegnare le definizioni dei metodi per specificare determinismo, sul comportamento delle chiamate Null e per specificare se un metodo è un mutatore. Per queste proprietà si presuppone l'uso dei valori predefiniti e l'attributo personalizzato viene utilizzato solo quando è necessario un valore non predefinito.
Nota
La classe SqlMethodAttribute
eredita dalla classe SqlFunctionAttribute
, pertanto SqlMethodAttribute
eredita i campi FillRowMethodName
e TableDefinition
da SqlFunctionAttribute
. Ciò implica che è possibile scrivere un metodo con valori di tabella, che non è il caso. Il metodo viene compilato e l'assembly viene distribuito, ma viene generato un errore relativo al tipo restituito IEnumerable
in fase di esecuzione con il messaggio seguente: "Metodo, proprietà o campo <name>
nella classe <class>
nell'assembly <assembly>
ha un tipo restituito non valido".
Nella tabella seguente vengono descritte alcune delle proprietà Microsoft.SqlServer.Server.SqlMethodAttribute
rilevanti che possono essere usate nei metodi definiti dall'utente e vengono elencati i valori predefiniti.
Proprietà | Descrizione |
---|---|
DataAccess |
Indica se la funzione implica l'accesso ai dati utente archiviati nell'istanza locale di SQL Server. Il valore predefinito è DataAccessKind.None . |
IsDeterministic |
Indica se la funzione produce gli stessi valori di output quando vengono specificati gli stessi valori di input e lo stesso stato del database. Il valore predefinito è false . |
IsMutator |
Indica se il metodo causa una modifica dello stato dell'istanza UDT. Il valore predefinito è false . |
IsPrecise |
Indica se la funzione comporta calcoli imprecisi, quali operazioni a virgola mobile. Il valore predefinito è false . |
OnNullCall |
Indica se il metodo viene chiamato quando vengono specificati argomenti di input con riferimento Null. Il valore predefinito è true . |
Esempio
La proprietà Microsoft.SqlServer.Server.SqlMethodAttribute.IsMutator
consente di contrassegnare un metodo che consente una modifica dello stato di un'istanza di un tipo definito dall'utente. Transact-SQL non consente di impostare due proprietà definite dall'utente nella clausola SET
di un'istruzione UPDATE
. È tuttavia possibile contrassegnare un metodo come mutatore che modifica i due membri.
Nota
I metodi mutator non sono consentiti nelle query. Tali metodi possono essere chiamati solo nelle istruzioni di assegnazione o nelle istruzioni di modifica dei dati. Se un metodo contrassegnato come mutatore non restituisce void
(o non è un Sub
in Visual Basic), CREATE TYPE
ha esito negativo con un errore.
L'istruzione seguente presuppone l'esistenza di un Triangles
tipo definito dall'utente con un metodo Rotate
. L'istruzione update seguente Transact-SQL richiama il metodo Rotate
:
UPDATE Triangles
SET t.RotateY(0.6)
WHERE id = 5;
Il metodo Rotate
viene decorato con l'impostazione dell'attributo SqlMethod
IsMutator
su true
in modo che SQL Server possa contrassegnare il metodo come metodo mutatore. Il codice imposta anche OnNullCall
su false
, che indica al server che il metodo restituisce un riferimento Null (Nothing
in Visual Basic) se uno dei parametri di input è un riferimento Null.
[SqlMethod(IsMutator = true, OnNullCall = false)]
public void Rotate(double anglex, double angley, double anglez)
{
RotateX(anglex);
RotateY(angley);
RotateZ(anglez);
}
Implementare un tipo definito dall'utente con un formato definito dall'utente
Quando si implementa un tipo definito dall'utente con un formato definito dall'utente, è necessario implementare Read
e Write
metodi che implementano l'interfaccia Microsoft.SqlServer.Server.IBinarySerialize
per gestire la serializzazione e la deserializzazione dei dati definiti dall'utente. È inoltre necessario specificare la proprietà MaxByteSize
del Microsoft.SqlServer.Server.SqlUserDefinedTypeAttribute
.
Il tipo definito dall'utente di valuta
Il Currency
tipo definito dall'utente è incluso negli esempi CLR che possono essere installati con SQL Server.
Il Currency
tipo definito dall'utente supporta la gestione di quantità di denaro nel sistema monetario di una determinata cultura. È necessario definire due campi: un string
per CultureInfo
, che specifica chi ha emesso la valuta (en-us
, ad esempio) e un decimal
per CurrencyValue
, l'importo di denaro.
Anche se non viene usato dal server per eseguire confronti, il tipo definito dall'utente di Currency
implementa l'interfaccia System.IComparable
, che espone un singolo metodo, System.IComparable.CompareTo
. Viene usato sul lato client in situazioni in cui è consigliabile confrontare o ordinare con precisione i valori di valuta all'interno delle impostazioni cultura.
Il codice eseguito in CLR confronta le impostazioni cultura separatamente dal valore della valuta. Per il codice Transact-SQL, le azioni seguenti determinano il confronto:
Impostare l'attributo
IsByteOrdered
su true, che indica a SQL Server di usare la rappresentazione binaria persistente su disco per i confronti.Usare il metodo
Write
per il tipo definito dall'utente diCurrency
per determinare in che modo il tipo definito dall'utente viene salvato in modo permanente su disco e quindi il modo in cui i valori definiti dall'utente vengono confrontati e ordinati per le operazioni di Transact-SQL.Salvare il
Currency
tipo definito dall'utente usando il formato binario seguente:Salvare le impostazioni cultura come stringa codificata UTF-16 per i byte 0-19 con riempimento a destra con caratteri Null.
Utilizzare i byte 20 e successivi per contenere il valore decimale della valuta.
Lo scopo della spaziatura interna è garantire che le impostazioni cultura siano completamente separate dal valore di valuta, in modo che quando un tipo definito dall'utente viene confrontato con un altro nel codice Transact-SQL, i byte delle impostazioni cultura vengono confrontati con i byte delle impostazioni cultura e i valori dei byte di valuta vengono confrontati con i valori di byte di valuta.
Attributi di valuta
Il Currency
tipo definito dall'utente viene definito con gli attributi seguenti.
[Serializable]
[SqlUserDefinedType(Format.UserDefined,
IsByteOrdered = true, MaxByteSize = 32)]
[CLSCompliant(false)]
public struct Currency : INullable, IComparable, IBinarySerialize
{ ... }
Creare metodi di lettura e scrittura con ibinaryserialize
Quando si sceglie UserDefined
formato di serializzazione, è necessario implementare anche l'interfaccia IBinarySerialize
e creare metodi Read
e Write
personalizzati. Le procedure seguenti del tipo definito dall'utente di Currency
usano il System.IO.BinaryReader
e System.IO.BinaryWriter
per leggere e scrivere nel tipo definito dall'utente.
// IBinarySerialize methods
// The binary layout is as follow:
// Bytes 0 - 19:Culture name, padded to the right
// with null characters, UTF-16 encoded
// Bytes 20+:Decimal value of money
// If the culture name is empty, the currency is null.
public void Write(System.IO.BinaryWriter w)
{
if (this.IsNull)
{
w.Write(nullMarker);
w.Write((decimal)0);
return;
}
if (cultureName.Length > cultureNameMaxSize)
{
throw new ApplicationException(string.Format(
CultureInfo.InvariantCulture,
"{0} is an invalid culture name for currency as it is too long.",
cultureNameMaxSize));
}
String paddedName = cultureName.PadRight(cultureNameMaxSize, '\0');
for (int i = 0; i < cultureNameMaxSize; i++)
{
w.Write(paddedName[i]);
}
// Normalize decimal value to two places
currencyValue = Decimal.Floor(currencyValue * 100) / 100;
w.Write(currencyValue);
}
public void Read(System.IO.BinaryReader r)
{
char[] name = r.ReadChars(cultureNameMaxSize);
int stringEnd = Array.IndexOf(name, '\0');
if (stringEnd == 0)
{
cultureName = null;
return;
}
cultureName = new String(name, 0, stringEnd);
currencyValue = r.ReadDecimal();
}