SQL Server CLR 使用 C# 自定義存儲過程和觸發器

這一篇博客接着上一篇博客繼續介紹 SQL CLR Stored Procedure 和 CLR Trigger,html

上一篇博客介紹了 SQL CLR Function 的使用,以及 CLR 程序集的註冊和 CLR Function 的註冊。sql

個人上一篇博客:SQL Server CLR 使用 C# 自定義函數數據庫

 

4、CLR Stored Procedurewindows

接下來在以前的項目選擇添加新項,選擇 SQL CLR C# 存儲過程。服務器

public partial class StoredProcedures
{
    /// <summary>
    /// 無輸入參數,無輸出參數,無輸出結果,有輸出消息,無返回值的存儲過程
    /// </summary>
    [Microsoft.SqlServer.Server.SqlProcedure(Name = "HelloWorld")]
    public static void HelloWorld()
    {
        SqlContext.Pipe.Send("Hello World");
    }

    /// <summary>
    /// 有輸入參數,無輸出參數,無輸出結果,無輸出消息,有返回值的存儲過程
    /// </summary>
    /// <param name="name"></param>
    [Microsoft.SqlServer.Server.SqlProcedure(Name = "GetStrLength")]
    public static SqlInt32 GetStrLength(SqlString str)
    {
        return str.ToString().Length;
    }

    /// <summary>
    /// 有輸入參數,有輸出參數,無輸出結果,無輸出消息,無返回值的存儲過程
    /// </summary>
    /// <param name="name"></param>
    [Microsoft.SqlServer.Server.SqlProcedure(Name = "SayHello")]
    public static void SayHello(SqlString name,out SqlString sayHello)
    {
        sayHello = "Hello " + name.ToString();
    }
}

註冊程序集和註冊存儲過程的 SQL 後面再貼出來,這裏咱們先看看結果。函數

PS:若是你用的是 Visual Studio 2015,那麼你能夠在【項目路徑>obj>Debug】文件夾下面找到自動生成的註冊程序集和存儲過程的 SQL 語句。至於其餘版本你們能夠試試。性能

執行存儲過程 HelloWorld:測試

--執行存儲過程 HelloWorld
exec [dbo].[HelloWorld]

結果:spa

這就是輸出消息,輸出消息和輸出結果是不同的,輸出消息是沒辦法獲取的(我沒辦法),而輸出結果就至關於用 Select 語句查詢出來的結果同樣,是能夠獲取的。3d

執行存儲過程 GetStrLength:

--執行存儲過程 GetStrLength
declare @res int 
exec @res=[dbo].[GetStrLength] '123456' 
select @res 

結果:

這個 C# 代碼裏面是有返回值的,也能夠經過這種方式獲取到返回值,可是這種返回值的方式只能返回 int 類型的返回值。

若是須要多個返回值呢?看下面的存儲過程,能夠經過設置多個輸出參數來達到。

執行存儲過程 SayHello:

--執行存儲過程 SayHello
declare @SayHello nvarchar(32)
exec [dbo].[SayHello] 'Brambling',@SayHello output 

select @SayHello

結果:

其實弄明白輸入參數、輸出參數、輸出消息、輸出結果和返回值這幾個問題,CLR 存儲過程的介紹就能夠完了。

可是存儲過程裏面老是免不了要操做數據的,那麼下面就看看對於數據庫數據的操做和輸出結果集的方法吧。

   /// <summary>
    /// 根據學生學號獲取學生姓名
    /// </summary>
    /// <param name="Id"></param>
    /// <returns></returns>
    [Microsoft.SqlServer.Server.SqlProcedure(Name = "GetStudentNameByStuNo")]
    public static void GetStudentNameByStuNo(SqlString stuNo,out SqlString stoName)
    {
        stoName = string.Empty;

        //由於程序是在SQL Server內執行,因此鏈接字符串寫成"context connection=true"便可
        using (SqlConnection conn = new SqlConnection("context connection=true"))
        {
            SqlCommand comm = new SqlCommand();
            comm.CommandText = "select StuName from StudentInfo where StuNo=@StuNo;";

            SqlParameter param = new SqlParameter("@StuNo", SqlDbType.NVarChar, 4000);
            param.SqlValue = stuNo;
            comm.Parameters.Add(param);

            comm.Connection = conn;
            conn.Open();
            SqlDataReader dataReader = comm.ExecuteReader();
            if (dataReader.Read())
            {
                stoName = dataReader.GetString(0);
            }
            dataReader.Close();
        }
    }

執行存儲過程 GetStudentNameByStuNo:

declare @StuName nvarchar(32)
exec [GetStudentNameByStuNo] 'A001',@StuName output 
select @StuName 
exec [GetStudentNameByStuNo] 'A003',@StuName output 
select @StuName 

結果:

能夠看到咱們經過輸出參數獲取到了返回值。若是如今我須要獲取整個學生的全部信息呢?

雖然能夠經過設置多個輸出參數獲得,可是學生信息的字段過多呢?下面看看輸出結果集的方式。

   /// <summary>
    /// 根據學生的學號獲取該學生的全部信息
    /// 返回的是一個結果集,即有多少條數據就返回多少條數據
    /// </summary>
    /// <param name="stuNo"></param>
    [Microsoft.SqlServer.Server.SqlProcedure(Name = "GetStudentInfoByStuNo_First")]
    public static void GetStudentInfoByStuNo_First(SqlString stuNo)
    {
        using (SqlConnection conn = new SqlConnection("context connection=true"))
        {
            SqlCommand comm = new SqlCommand();
            comm.CommandText = "select ID,StuNo,StuName,StuAge from StudentInfo where StuNo=@StuNo;";

            SqlParameter param = new SqlParameter("@StuNo", SqlDbType.NVarChar, 4000);
            param.SqlValue = stuNo;
            comm.Parameters.Add(param);

            comm.Connection = conn;
            conn.Open();
            SqlDataReader dataReader = comm.ExecuteReader();
            SqlContext.Pipe.Send(dataReader);
            dataReader.Close();
        }
    }

    /// <summary>
    /// 根據學生的學號獲取該學生的全部信息
    /// 這種方式效率比較高,是經過直接執行 SqlCommand 指令,而後把數據發送到客戶端,不須要通過託管內存
    /// </summary>
    /// <param name="stuNo"></param>
    [Microsoft.SqlServer.Server.SqlProcedure(Name = "GetStudentInfoByStuNo_Second")]
    public static void GetStudentInfoByStuNo_Second(SqlString stuNo)
    {
        using (SqlConnection conn = new SqlConnection("context connection=true"))
        {
            SqlCommand comm = new SqlCommand();
            comm.CommandText = "select ID,StuNo,StuName,StuAge from StudentInfo where StuNo=@StuNo;";

            SqlParameter param = new SqlParameter("@StuNo", SqlDbType.NVarChar, 4000);
            param.SqlValue = stuNo;
            comm.Parameters.Add(param);

            comm.Connection = conn;
            conn.Open();
            SqlContext.Pipe.ExecuteAndSend(comm);
        }
    }

    /// <summary>
    /// 根據學生的學號獲取該學生的全部信息
    /// </summary>
    /// <param name="stuNo"></param>
    [Microsoft.SqlServer.Server.SqlProcedure(Name = "GetStudentInfoByStuNo_Third")]
    public static void GetStudentInfoByStuNo_Third(SqlString stuNo)
    {
        using (SqlConnection conn = new SqlConnection("context connection=true"))
        {
            SqlCommand comm = new SqlCommand();
            comm.CommandText = "select ID,StuNo,StuName,StuAge from StudentInfo where StuNo=@StuNo;";

            SqlParameter param = new SqlParameter("@StuNo", SqlDbType.NVarChar, 4000);
            param.SqlValue = stuNo;
            comm.Parameters.Add(param);

            comm.Connection = conn;
            conn.Open();
            SqlDataReader dataReader = comm.ExecuteReader();

            SqlDataRecord dataRecord = new SqlDataRecord(
                new SqlMetaData[]
                {
                    new SqlMetaData("ID",SqlDbType.Int),
                    new SqlMetaData("StuNo",SqlDbType.NVarChar,128),
                    new SqlMetaData("StuName",SqlDbType.NVarChar,128),
                    new SqlMetaData("StuAge",SqlDbType.Int)
                }
            );

            if(dataReader.Read())
            {
                dataRecord.SetInt32(0,(int)dataReader["ID"]);
                dataRecord.SetString(1,(string)dataReader["StuNo"]);
                dataRecord.SetString(2,(string)dataReader["StuName"]);
                dataRecord.SetInt32(3,(int)dataReader["StuAge"]);
                SqlContext.Pipe.Send(dataRecord);
            }
            dataReader.Close();
        }
    }

執行存儲過程:

--執行存儲過程 GetStudentInfoByStuNo_First
exec [GetStudentInfoByStuNo_First] 'A003'

--執行存儲過程 GetStudentInfoByStuNo_Second
exec [GetStudentInfoByStuNo_Second] 'A003'

--執行存儲過程 GetStudentInfoByStuNo_Third
exec [GetStudentInfoByStuNo_Third] 'A003'

結果:

上面三個方法中,第一個方法和第二個方法都是直接返回查詢結果的,可是在實際存儲過程中是不會這樣寫的,裏面應該包含有邏輯操做等等,因此就有了第三個方法。

那麼如今是返回的一條數據,若是是返回多條數據呢?第一種方法和第二種方法就不說了,由於這兩種方法都是返回結果集的。

   /// <summary>
    /// 根據年齡查詢學生的信息
    /// 這種方式是一條數據返回一個結果集
    /// </summary>
    /// <param name="stuAge"></param>
    [Microsoft.SqlServer.Server.SqlProcedure(Name = "GetStudentsInfoByStuAge_Single")]
    public static void GetStudentsInfoByStuAge_Single(SqlInt32 stuAge)
    {
        using (SqlConnection conn = new SqlConnection("context connection=true"))
        {
            SqlCommand comm = new SqlCommand();
            comm.CommandText = "select ID,StuNo,StuName,StuAge from StudentInfo where StuAge=@StuAge;";

            SqlParameter param = new SqlParameter("@StuAge", SqlDbType.Int);
            param.SqlValue = stuAge;
            comm.Parameters.Add(param);

            comm.Connection = conn;
            conn.Open();
            SqlDataReader dataReader = comm.ExecuteReader();

            SqlDataRecord dataRecord = new SqlDataRecord(
                new SqlMetaData[]
                {
                    new SqlMetaData("ID",SqlDbType.Int),
                    new SqlMetaData("StuNo",SqlDbType.NVarChar,128),
                    new SqlMetaData("StuName",SqlDbType.NVarChar,128),
                    new SqlMetaData("StuAge",SqlDbType.Int)
                }
            );

            while (dataReader.Read())
            {
                dataRecord.SetInt32(0, (int)dataReader["ID"]);
                dataRecord.SetString(1, (string)dataReader["StuNo"]);
                dataRecord.SetString(2, (string)dataReader["StuName"]);
                dataRecord.SetInt32(3, (int)dataReader["StuAge"]);
                //發送結果集到客戶端
                SqlContext.Pipe.Send(dataRecord);
            }
            dataReader.Close();
        }
    }

    /// <summary>
    /// 根據年齡查詢學生的信息
    /// 這種方式是全部的數據返回一個結果集
    /// </summary>
    /// <param name="stuAge"></param>
    [Microsoft.SqlServer.Server.SqlProcedure(Name = "GetStudentsInfoByStuAge_Multiple")]
    public static void GetStudentsInfoByStuAge_Multiple(SqlInt32 stuAge)
    {
        using (SqlConnection conn = new SqlConnection("context connection=true"))
        {
            SqlCommand comm = new SqlCommand();
            comm.CommandText = "select ID,StuNo,StuName,StuAge from StudentInfo where StuAge=@StuAge;";

            SqlParameter param = new SqlParameter("@StuAge", SqlDbType.Int);
            param.SqlValue = stuAge;
            comm.Parameters.Add(param);

            comm.Connection = conn;
            conn.Open();
            SqlDataReader dataReader = comm.ExecuteReader();

            SqlDataRecord dataRecord = new SqlDataRecord(
                new SqlMetaData[]
                {
                    new SqlMetaData("ID",SqlDbType.Int),
                    new SqlMetaData("StuNo",SqlDbType.NVarChar,128),
                    new SqlMetaData("StuName",SqlDbType.NVarChar,128),
                    new SqlMetaData("StuAge",SqlDbType.Int)
                }
            );

            //標記結果集的開始
            SqlContext.Pipe.SendResultsStart(dataRecord);
            while (dataReader.Read())
            {
                dataRecord.SetInt32(0, (int)dataReader["ID"]);
                dataRecord.SetString(1, (string)dataReader["StuNo"]);
                dataRecord.SetString(2, (string)dataReader["StuName"]);
                dataRecord.SetInt32(3, (int)dataReader["StuAge"]);
                //填充數據到結果集
                SqlContext.Pipe.SendResultsRow(dataRecord);
            }
            //標記結果集的結束
            SqlContext.Pipe.SendResultsEnd();
            dataReader.Close();
        }
    }

執行存儲過程:

--執行存儲過程 GetStudentsInfoByStuAge_Single
exec [dbo].[GetStudentsInfoByStuAge_Single] '18'

--執行存儲過程 GetStudentsInfoByStuAge_Multiple
exec [dbo].[GetStudentsInfoByStuAge_Multiple] '18'

結果:

能夠很清楚的看到,方法一是一條數據返回一個結果集,方法二是全部數據返回一個結果集。

下面貼出註冊存儲過程的 SQL 語句,註冊程序集的就不貼了,個人上一篇博客有過介紹。

--註冊存儲過程 HelloWorld
CREATE PROCEDURE [dbo].[HelloWorld] 
WITH EXECUTE AS CALLER
AS 
EXTERNAL NAME [UserDefinedSqlClr].[StoredProcedures].[HelloWorld];    --EXTERNAL NAME 程序集名.類名.方法名
GO --註冊存儲過程 GetStrLength CREATE PROCEDURE [dbo].[GetStrLength] @str [nvarchar](MAX) WITH EXECUTE AS CALLER AS EXTERNAL NAME [UserDefinedSqlClr].[StoredProcedures].[GetStrLength]; --EXTERNAL NAME 程序集名.類名.方法名
GO --註冊存儲過程 SayHello CREATE PROCEDURE [dbo].[SayHello] @name [nvarchar](MAX), @sayHello [nvarchar](MAX) OUTPUT WITH EXECUTE AS CALLER AS EXTERNAL NAME [UserDefinedSqlClr].[StoredProcedures].[SayHello]; --EXTERNAL NAME 程序集名.類名.方法名
GO --註冊存儲過程 GetStudentNameByStuNo CREATE PROCEDURE [dbo].[GetStudentNameByStuNo] @stuNo [nvarchar](MAX), @stoName [nvarchar](MAX) OUTPUT WITH EXECUTE AS CALLER AS EXTERNAL NAME [UserDefinedSqlClr].[StoredProcedures].[GetStudentNameByStuNo]; --EXTERNAL NAME 程序集名.類名.方法名
GO --註冊存儲過程 GetStudentInfoByStuNo_First CREATE PROCEDURE [dbo].[GetStudentInfoByStuNo_First] @stuNo [nvarchar](MAX) WITH EXECUTE AS CALLER AS EXTERNAL NAME [UserDefinedSqlClr].[StoredProcedures].[GetStudentInfoByStuNo_First]; --EXTERNAL NAME 程序集名.類名.方法名
GO --註冊存儲過程 GetStudentInfoByStuNo_Second CREATE PROCEDURE [dbo].[GetStudentInfoByStuNo_Second] @stuNo [nvarchar](MAX) WITH EXECUTE AS CALLER AS EXTERNAL NAME [UserDefinedSqlClr].[StoredProcedures].[GetStudentInfoByStuNo_Second]; --EXTERNAL NAME 程序集名.類名.方法名
GO --註冊存儲過程 GetStudentInfoByStuNo_Third CREATE PROCEDURE [dbo].[GetStudentInfoByStuNo_Third] @stuNo [nvarchar](MAX) WITH EXECUTE AS CALLER AS EXTERNAL NAME [UserDefinedSqlClr].[StoredProcedures].[GetStudentInfoByStuNo_Third]; --EXTERNAL NAME 程序集名.類名.方法名
GO --註冊存儲過程 GetStudentsInfoByStuAge_Single CREATE PROCEDURE [dbo].[GetStudentsInfoByStuAge_Single] @stuAge [int] WITH EXECUTE AS CALLER AS EXTERNAL NAME [UserDefinedSqlClr].[StoredProcedures].[GetStudentsInfoByStuAge_Single]; --EXTERNAL NAME 程序集名.類名.方法名
GO --註冊存儲過程 GetStudentsInfoByStuAge_Multiple CREATE PROCEDURE [dbo].[GetStudentsInfoByStuAge_Multiple] @stuAge [int] WITH EXECUTE AS CALLER AS EXTERNAL NAME [UserDefinedSqlClr].[StoredProcedures].[GetStudentsInfoByStuAge_Multiple]; --EXTERNAL NAME 程序集名.類名.方法名 GO

 

5、CLR Trigger

接下來選擇添加新項,選擇 SQL CLR C# 觸發器。

一、DML 觸發器

(1) after trigger

public partial class Triggers
{
    /// <summary>
    /// 輸出操做的數據
    /// </summary>
    [Microsoft.SqlServer.Server.SqlTrigger(Name = "FirstSqlTrigger", Target = "StudentInfo", Event = "FOR INSERT,UPDATE,DELETE")]
    public static void FirstSqlTrigger()
    {
        switch (SqlContext.TriggerContext.TriggerAction)
        {
            case TriggerAction.Insert:
                GetInsertedOrDeleted(InsOrDel.Inserted);
                break;
            case TriggerAction.Update:
                GetInsertedOrDeleted(InsOrDel.Inserted);
                GetInsertedOrDeleted(InsOrDel.Deleted);
                break;
            case TriggerAction.Delete:
                GetInsertedOrDeleted(InsOrDel.Deleted);
                break;
            default:
                break;
        }
    }

    /// <summary>
    /// 獲取操做的數據或以後的數據
    /// </summary>
    /// <param name="insOrDel"></param>
    /// <returns></returns>
    private static void GetInsertedOrDeleted(InsOrDel insOrDel)
    {
        using (SqlConnection conn = new SqlConnection("context connection=true"))
        {
            SqlCommand comm = new SqlCommand();
            comm.CommandText = "select ID,StuNo,StuName,StuAge from " + insOrDel.ToString() + ";";
            comm.Connection = conn;
            conn.Open();
            SqlDataReader dataReader = comm.ExecuteReader();

            SqlDataRecord dataRecord = new SqlDataRecord(
                new SqlMetaData[]
                {
                    new SqlMetaData("ID",SqlDbType.Int),
                    new SqlMetaData("StuNo",SqlDbType.NVarChar,128),
                    new SqlMetaData("StuName",SqlDbType.NVarChar,128),
                    new SqlMetaData("StuAge",SqlDbType.Int)
                }
            );

            if (dataReader.Read())
            {
                dataRecord.SetInt32(0, (int)dataReader["ID"]);
                dataRecord.SetString(1, (string)dataReader["StuNo"]);
                dataRecord.SetString(2, (string)dataReader["StuName"]);
                dataRecord.SetInt32(3, (int)dataReader["StuAge"]);
                //發送結果集到客戶端
                SqlContext.Pipe.Send(dataRecord);
            }
            dataReader.Close();
        }
    }

    private enum InsOrDel
    {
        Inserted,
        Deleted
    }
}

測試 SQL 語句:

  -- Insert 操做
  insert into StudentInfo(StuNo,StuName,StuAge)
  values('A006','小飛',20)

  -- Update 操做
  update StudentInfo set StuName='小飛飛' where StuNo='A006' 

  -- Delete 操做
  delete from StudentInfo where StuNo='A006'

結果:

這裏說明一下,Microsoft.SqlServer.Server.SqlTrigger 有三個屬性。

Name:表示觸發器的名稱。

Target:表示觸發器的目標表的名稱。

Event:表示觸發執行觸發器的動做。

 

(2) instead of trigger

public partial class Triggers
{
    /// <summary>
    /// 輸出操做類型
    /// </summary>
    [Microsoft.SqlServer.Server.SqlTrigger(Name = "InsteadOfTrigger",Target = "StudentInfo",Event = "INSTEAD OF INSERT,UPDATE,DELETE")]
    public static void InsteadOfTrigger()
    {
        SqlDataRecord dataRecord = new SqlDataRecord(
            new SqlMetaData[]
            {
                new SqlMetaData("Message",SqlDbType.NVarChar,128)
            }
        );

        switch (SqlContext.TriggerContext.TriggerAction)
        {
            case TriggerAction.Insert:
                dataRecord.SetString(0, "Insert操做");
                break;
            case TriggerAction.Update:
                dataRecord.SetString(0, "Update操做");
                break;
            case TriggerAction.Delete:
                dataRecord.SetString(0, "Delete操做");
                break;
            default:
                dataRecord.SetString(0, "Nothing");
                break;
        }
        SqlContext.Pipe.Send(dataRecord);
    }
}

測試 SQL 語句:

-- Insert 操做
insert into StudentInfo(StuNo,StuName,StuAge)
values('A006','小飛',20)

-- Update 操做
update StudentInfo set StuName='小飛飛' where StuNo='A006' 

-- Delete 操做
delete from StudentInfo where StuNo='A006'

結果:

Instead of 是一種特殊的觸發器,它只執行觸發器自己,也就是觸發器裏面的操做,

因此 Insert、Update、Delete 操做是不執行的,只是用於觸發該觸發器,並且 Instead of 觸發器會覆蓋掉 after 觸發器。

 

二、DDL 觸發器

DDL 觸發器又分爲數據庫級別的觸發器和服務器級別的觸發器,這裏只介紹數據庫級別的觸發器。

public partial class Triggers
{
    /// <summary>
    /// 禁止刪除表和刪除存儲過程的 DDL 觸發器
    /// </summary>
    [Microsoft.SqlServer.Server.SqlTrigger(Name = "SecondSqlTrigger")]
    public static void SecondSqlTrigger()
    {
        switch (SqlContext.TriggerContext.TriggerAction)
        {
            case TriggerAction.DropTable:
                try
                {
                    Transaction tran = Transaction.Current;
                    tran.Rollback();
                }
                catch
                {
                }
                SqlContext.Pipe.Send("You have no authority");
                break;
            case TriggerAction.DropProcedure:
                try
                {
                    Transaction tran = Transaction.Current;
                    tran.Rollback();
                }
                catch
                {
                }
                SqlContext.Pipe.Send("You have no authority");
                break;
            default:
                break;
        }
    }
}

這裏 DDL 的觸發器,只須要指定觸發器名稱的屬性就能夠了。

測試 SQL 語句:

--刪除表 StudentInfo
drop table StudentInfo

結果:

下面貼出註冊觸發器的 SQL 語句。

--註冊觸發器 FirstSqlTrigger
CREATE TRIGGER [FirstSqlTrigger] 
ON StudentInfo    --目標表
FOR INSERT,UPDATE,DELETE        --指定觸發的操做
AS 
EXTERNAL NAME [UserDefinedSqlClr].[Triggers].[FirstSqlTrigger];    --EXTERNAL NAME 程序集名.類名.方法名
GO

--註冊觸發器 InsteadOfTrigger
CREATE TRIGGER [InsteadOfTrigger] 
ON StudentInfo    --目標表
INSTEAD OF INSERT,UPDATE,DELETE        --指定觸發的操做
AS 
EXTERNAL NAME [UserDefinedSqlClr].[Triggers].[InsteadOfTrigger];    --EXTERNAL NAME 程序集名.類名.方法名
GO

--註冊觸發器 SecondSqlTrigger
CREATE TRIGGER [SecondSqlTrigger] 
ON database  --數據庫級別觸發器
for drop_table,drop_procedure        --指定觸發的操做
AS 
EXTERNAL NAME [UserDefinedSqlClr].[Triggers].[SecondSqlTrigger];    --EXTERNAL NAME 程序集名.類名.方法名
GO

刪除存儲過程和刪除觸發器的 SQL 語句相似,惟一須要注意的就是刪除數據庫級別的觸發器時,須要在後面加上 on database,例如:

--刪除數據庫級別觸發器 SecondSqlTrigger
drop trigger [SecondSqlTrigger] on database

其實觸發器自己就不多用到,由於對於數據量大的時候,特別影響性能,因此這裏很少作介紹。

能夠參考這裏:CLR 觸發器

 

6、總結

總算寫完了。。。

其實 CLR 自定義函數、存儲過程和觸發器等,不必定比 T-SQL 好用,準確來講性能稍微差點。

可是這只是提供一種方法,遇到 T-SQL 不能解決時能夠考慮的一種方法。

畢竟瞭解的越多,會的越多,遇到問題處理的方法就越多。

相關文章
相關標籤/搜索