Inside the method body of a managed trigger, you need to get a reference to the execu-tion context of the trigger so you can find out what Data Manipulaexecu-tion Language DML statement
Trang 1IsNullIfEmpty—Tells SQL Server that the UDA will return null if its aggregated
value is empty (that is, if its value is 0, or the empty string ””, and so on)
Name—Tells the deployment routine what to call the UDA when it is created in
the database
MaxByteSize—Tells SQL Server not to allow more than the specified number of
bytes to be held in an instance of the UDA You must specify this when using
Format.UserDefined
For this example, you implement a very simple UDA that sums values in an integer
column, but only if they are prime Listing 46.8 shows the code to do this
LISTING 46.8 A UDA That Sums Prime Numbers
using System;
using System.Data;
using System.Data.Sql;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
[Serializable]
[Microsoft.SqlServer.Server.SqlUserDefinedAggregate(
Format.Native,
IsInvariantToDuplicates=false,
IsInvariantToNulls=true,
IsInvariantToOrder=true,
IsNullIfEmpty=true
)]
public struct SumPrime
{
SqlInt64 Sum;
private bool IsPrime(SqlInt64 Number)
{
for (int i = 2; i < Number; i++)
{
if (Number % i == 0)
{
return false;
}
}
return true;
}
public void Init()
{
Sum = 0;
Trang 2}
public void Accumulate(SqlInt64 Value)
{
if (!Value.IsNull && IsPrime(Value) && Value > 1)
Sum += Value;
}
public void Merge(SumPrime Prime)
{
Sum += Prime.Sum;
}
public SqlInt64 Terminate()
{
return Sum;
}
}
In this code, SQL Server first calls Init(), initializing the private Sumdata field to 0
For each column value passed to the aggregate, the Accumulate()method is called,
whereinSumis increased by the value of the column, if it is prime
When multiple threads converge, Merge()is called, adding the values stored in each
instance (as the Primeparameter) to Sum
When the rowset has been completely parsed, SQL Server calls Terminate(), wherein the
accumulated value Sumis returned
Following are the results of testing SumPrimeonProduction.Product(an existing
AdventureWorks2008table):
SELECT TOP 10 dbo.SumPrime(p.ProductId) AS PrimeSum, p.Name
FROM Production.Product p
JOIN Production.WorkOrder o ON
o.ProductId = p.ProductId
WHERE Name LIKE ‘%Frame%’
GROUP BY p.ProductId, p.Name
ORDER BY PrimeSum DESC
go
PrimeSum Name
-360355 HL Mountain Frame - Black, 42
338462 HL Mountain Frame - Silver, 42
266030 HL Road Frame - Red, 48
214784 HL Road Frame - Black, 48
Trang 3133937 HL Touring Frame - Yellow, 46
68338 LL Road Frame - Red, 52
54221 LL Mountain Frame - Silver, 48
15393 ML Road Frame - Red, 52
0 HL Mountain Frame - Black, 38
0 HL Road Frame - Black, 44
(10 row(s) affected.)
Following is the DDL syntax for this UDA:
CREATE AGGREGATE SumPrime(@Number bigint)
RETURNS bigint
EXTERNAL NAME SQLCLR.SumPrime
As with UDTs, with UDAs there is no ALTER AGGREGATE, but you can use DROP AGGREGATE
to drop them
Developing Managed Triggers
Managed triggers are static methods of a NET class decorated with the SqlTrigger
attribute.SqlTriggerhas three named parameters:
Event—A required string-valued parameter that tells SQL Server which type of trigger
you’re defining, as is done when defining T-SQL triggers
Target—A required string-valued parameter that tells SQL Server which schema and
table you’re attaching the trigger to
Name—An optional string parameter that tells the deployment routine what to call the
trigger when it is created in the database
The implementation contract for a managed trigger is only that it be a static method that
returnsvoid
Inside the method body of a managed trigger, you need to get a reference to the
execu-tion context of the trigger so you can find out what Data Manipulaexecu-tion Language (DML)
statement the trigger is responding to and which columns have been updated You do
this by using theSqlContext.TriggerContextobject of typeSqlTriggerContext (Note
that this object is null when used in nontrigger contexts.) It has the following members:
ColumnCount—An integer property that indicates how many columns were affected
by the operation
IsUpdatedColumn—A Boolean method that indicates whether the column at a
specific position was updated during the operation
TriggerAction—An enum that indicates which operation caused the trigger to fire
For DML triggers, this is either TriggerAction.Insert,TriggerAction.Update, or
TriggerAction.Delete For DDL triggers, the list is quite a bit longer Refer to MSDN
to see all the possible values of the TriggerActionenumeration
Trang 4EventData—In the case of a DDL trigger, an object of type SqlXmlthat contains an XML
document whose content explains the DDL that just fired (The XML content model for
this object is the same as that returned by the EVENTDATA()built-in function.)
Have you ever wanted to be notified by email that some important column value in your
tables has been created or updated? There are many ways to do this, including using
Query Notifications You can also accomplish this by writing a managed trigger that calls a
web service, which in turn sends an email
Up until now, you haven’t had to decrease the runtime safety of your assembly But because
certain aspects of web services use theSynchronizedattribute (which means they do thread
synchronization), we have to change our SQLCLR assembly’s permission set toUNSAFE
CAUTION
Only the sysadminrole can upload an UNSAFEassembly to SQL Server You should
allow this uploading only when you know the code being uploaded doesn’t do anything
that might compromise the integrity of the data, the server, or your job
First, you need to create a simple web service routine that sends your email To do this
using Visual Studio 2008, you create a new local IIS website called photoserveand add to
it a new web service called PhotoService.asmx Then you replace the entire body of
PhotoService.cswith the following C# code:
using System;
using System.Web.Services;
using System.Net.Mail;
using System.Configuration;
[WebService(Namespace = “urn:www-samspublishing-com:examples:sqlclr:triggers”)]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class PhotoService : System.Web.Services.WebService
{
[WebMethod]
public void PhotoUpdateNotify(int ProductPhotoId)
{
MailMessage m = new MailMessage();
m.Subject = “New Photo: “ + ProductPhotoId.ToString();
m.From = new MailAddress(“ProductPhotoService@localservername”);
m.Body = “http://localhost:1347/photoserve/getphoto.aspx?ppid=” +
ProductPhotoId.ToString();
m.To.Add(new MailAddress(“PhotoAdmin@ localservername “));
SmtpClient s = new SmtpClient(“localservername”, 25);
s.Send(m);
}
}
Trang 5Of course, you need to have SMTP and IIS correctly configured on your server for this
example to work completely You also need to replace localhostandlocalservername
and the email account names shown in the code with values that work for you
Next, you should add a new web form called getphoto.aspxto the site You replace the
entire contents of getphoto.aspx.cswith the code in Listing 46.9
LISTING 46.9 A Web Form That Retrieves Photos from SQL Server
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Data.SqlClient;
using System.IO;
public partial class getphoto : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (Request.QueryString[“ppid”] != null)
{
string ppid = Request.QueryString[“ppid”].ToString();
string FileName = “photos/” + ppid + “.jpeg”;
string MappedFileName = Server.MapPath(FileName);
using (SqlConnection c =
new SqlConnection(
“Data Source=(local);Initial Catalog=AdventureWorks2008;
Integrated Security=True”
)
)
{
using (SqlCommand s = new SqlCommand(
@”SELECT LargePhoto
FROM Production.ProductPhoto
WHERE ProductPhotoId = “ + ppid, c))
{
c.Open();
using (SqlDataAdapter a = new SqlDataAdapter(s))
{
using (DataSet d = new DataSet())
{
a.Fill(d);
if (d.Tables.Count == 1 && d.Tables[0].Rows.Count == 1)
{
Trang 6byte[] BigImg = (byte[])d.Tables[0].Rows[0][“LargePhoto”];
FileStream f = new FileStream(
MappedFileName, FileMode.Create, FileAccess.Write);
f.Write(BigImg, 0, BigImg.GetUpperBound(0));
f.Close();
Response.Redirect(FileName, false);
}
else
{
Response.Write(“<H2>Sorry, ProductPhotoId “ + ppid + “ was not found.</H2>”);
}
}
}
}
}
}
else
{
Response.Write(“<H2>A querystring value for ppid is required.</H2>”);
}
}
}
Next, you add a subfolder to the site called photos This is the place where the web form
will save product photos as JPEG files and redirect the email recipient The main body of
the code in Listing 46.9 illustrates how to save LOB values to file in a succinct manner, so
it may prove useful for your other applications
You either need to give your ASP.NET user file I/O permissions on photosor have the web
application impersonate a user who has those permissions
To recap, the website code so far consists of the following: a web service
(PhotoService.asmx) that generates notification emails containing URLs These URLs in
turn point to a web form (getphoto.aspx) that saves the varbinaryvalue of
Production.ProductPhoto.LargePhoto(given a particular ProductPhotoId) to the photos
folder as [ProductPhotoId].jpeg
The last item you need is the reason you’re writing this code in the first place: a managed
trigger that invokes the web service to kick off the whole process To add this, you
right-click the SQLCLR project and then select Add, Trigger Name this new trigger class
Triggers.cs(the default) Then replace the entire content of Triggers.cswith the code
in Listing 46.10
Trang 7LISTING 46.10 A Managed Trigger That Invokes a Web Service
using System;
using System.Data;
using Microsoft.SqlServer.Server;
using System.Data.SqlClient;
using SQLCLR.photoserve;
public partial class Triggers
{
[Microsoft.SqlServer.Server.SqlTrigger(
Event = “FOR UPDATE”,
Name = “Production.PhotoUpdateTrigger”,
Target = “Production.ProductPhoto”
)]
public static void PhotoUpdateTrigger()
{
SqlTriggerContext stc = SqlContext.TriggerContext;
if (stc.TriggerAction == TriggerAction.Update)
{
if (stc.IsUpdatedColumn(3)) //The LargePhoto varbinary(max) column
{
using (SqlCommand s = new SqlCommand(
“SELECT DISTINCT ProductPhotoId FROM INSERTED”,
new SqlConnection(“context connection=true”)))
{
s.Connection.Open();
using (SqlDataReader r =
s.ExecuteReader(CommandBehavior.CloseConnection))
{
PhotoService p = new PhotoService();
while (r.Read())
{
SqlContext.Pipe.Send(
“Notifying Web Service of Update for PPID: “ + r.GetInt32(0).ToString());
p.PhotoUpdateNotify(r.GetInt32(0));
}
}
}
}
}
}
}
Trang 8Now that all the code is in place, all that’s left is an explanation of the code of
PhotoUpdateTrigger()and a test case
In the code in Listing 46.10, you check to see whether the current TriggerActionis
TriggerAction.Update, meaning that the trigger is firing due to an update You declare
this to be trueby using the Eventnamed parameter of the SqlTriggerattribute
Next, you select the ProductPhotoIdof the updated row from the INSERTEDtable and
connect to the database by using the context connection
You execute the command and get your SqlDataReader(r); then you instantiate the
PhotoServiceweb service Using the overloaded method of the Pipeobject, you send a
string literal informational message (equivalent to T-SQL’s printfunction), which tells any
clients what is about to happen You call the PhotoUpdateNotifymethod of the web
service and pass in the ProductPhotoId, which in turn sends the email containing the link
back to getphoto.aspx, which generates the photo JPEG for that ProductPhotoId
To make the test case work, you need to make your local machine’s Network Service user a
SQL Server login and a user in AdventureWorks2008with at least db_datareaderaccess In
addition, you might need to use the Visual Studio sgen.exetool to create a serialization
assembly for SQL2008SQLCLR.dll(whichsgen.exewould, by default, name
SQL2008SQLCLR.XmlSerializers.dll)
You need to load this serialization assembly into AdventureWorks2008before loading the
main assembly (using CREATE ASSEMBLY) (At the time of this writing, it was necessary to
also load System.Web.dlland its dependencies into AdventureWorks2008before loading
the application assemblies.)
To test the trigger, you simply update a value of Production.ProductPhoto.LargePhoto:
UPDATE Production.ProductPhoto
SET LargePhoto = LargePhoto
WHERE ProductPhotoId = 69
go
Notifying Web Service of Update for PPID: 69
(1 row(s) affected.)
If you get an email in your test inbox, you’ve done everything right If not, don’t fret; this
is a challenging example developed mainly to show the power of managed code
Using Transactions
When you are writing managed objects, just as with T-SQL, it’s important to be aware of the
current transaction context under which your code may be running
Managed database objects have the option of making use of the classes in the new
System.Transactionsnamespace to control transactions Following are the main objects
you use to do this:
Transaction.Current—This is a static object of type Transactionthat represents the
current transaction You use this object to explicitly roll back the current transaction
Trang 9(usingRollback()) It contains an IsolationLevelproperty that indicates the
current transaction isolation level, as well as a TransactionCompletedevent that
your objects may subscribe to and a TransactionInformationproperty that indicates
TransactionStatusand other attributes of the transaction You can also use this
object to manually enlist additional objects in the current transaction
TransactionScope—This object represents a transactional scope that is used to wrap
managed code Note that transactions automatically roll back unless they are
explic-itly committed using this object’sComplete()method It is enough to merely
instantiate this object at the beginning of the managed code: If a current transaction
is active, the instantiated object assumes that transaction; if not, a new transaction
is initiated
Note that it is not necessary to explicitly declare or even use transactions: if your managed
code is already running in the scope of a transaction, it automatically participates in that
transaction (To turn off this behavior, you append ”enlist=false”to your connection
string.) In fact, even if your code opens additional connections on additional servers, the
transaction context is not only preserved but is automatically promoted to a distributed
transaction that enlists all the connections involved (The MSDTC service must be
running for distributed transactions to work.)
One thing you cannot do with managed transactions that you can with T-SQL is begin a
new transaction and then just leave it open
The code example in Listing 46.11 illustrates the use of theSystem.Transactionsobjects
in a managed stored procedure You need to add a new managed stored procedure to the
SQLCLR project and call itSPTrans Then you need to add theusingstatementusing
System.Transactions;and replace the autogenerated method with the code from
Listing 46.11
LISTING 46.11 Using Transactions in a Managed Stored Procedure
[SqlProcedure]
public static void SpTrans()
{
TransactionScope ts = null;
try
{
SqlContext.Pipe.Send(“Proc Started”);
if (Transaction.Current != null)
{
SqlContext.Pipe.Send(“A) Current tran is not null.”);
SqlContext.Pipe.Send(“A) About to rollback current tran ”);
Transaction.Current.Rollback(
new ApplicationException(“I wanted to do this.”));
SqlContext.Pipe.Send(“A) Rollback Complete.”);
}
else
Trang 10{
SqlContext.Pipe.Send(“A) Current tran is null.”);
}
ts = new System.Transactions.TransactionScope();
SqlContext.Pipe.Send(“New Tran Started”);
if (Transaction.Current != null)
SqlContext.Pipe.Send(“B) Current tran is not null.”);
else
SqlContext.Pipe.Send(“B) Current tran is null.”);
if (ts != null)
ts.Complete();
SqlContext.Pipe.Send(“B) Complete() is Complete.”);
}
finally
{
if (ts != null)
ts.Dispose();
SqlContext.Pipe.Send(“Proc Complete”);
}
}
To test this code, you simply run the stored procedure from a query window (or use
sqlcmd.exe) inside and outside a transactional scope and watch the results Here’s an
example:
BEGIN TRAN
EXEC dbo.SpTrans
ROLLBACK TRAN
EXEC dbo.SPTrans
Using the Related System Catalogs
As with other database objects, SQL Server provides catalog views that enable you to view
loaded managed assemblies, routines, and types The base view for finding these objects is
sys.assemblies
To see which assemblies have been loaded (including the one you created in this
chapter), you use the following query:
SELECT TOP 5
name,
assembly_id,
permission_set_desc as permission_set
FROM sys.assemblies