分享自己存取資料庫使用的SqlHelper類別,ADO.net技術

此SqlHelper.cs已經把ADO.net存取資料庫技術都封裝完整,算是Data Access Layer的東西

它簡化ADO.net複雜語法、讓人可以專注寫SQL指令來存取資料庫

雖然目前最潮的存取資料庫技術是Entity Framework,不過仍然有些維護案的資料存取層寫的眼花繚亂又很抽象

這時候我就會拿出這套類別,直搗黃龍地撈資料XD

程式碼已在我的工作專案上運行2年左右,該調整的都調過了

丟上來分享兼備份

前置作業

在專案裡的App.config或Web.config裡(沒有的話要先對專案加入新增項目→找組態檔)
先配置連線字串

<configuration>
  <connectionStrings>
    <!--給SqlHelper用的連線-->
    <add name="dbConn" connectionString="Data Source=.\sqlexpress2012;Initial Catalog=TestDB;Integrated Security=True;MultipleActiveResultSets=True;Max Pool Size=1000;" />
  </connectionStrings>
</configuration>

接著專案加入參考:System.Configuration,如果存取資料庫是Oracle的話,也要加入System.Data.OracleClient的參考

然後新增一個類別檔名為SqlHelper.cs,把以下程式碼都貼上去

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Collections;
using System.Data;
using System.Data.SqlClient;
/*引用*/
using System.Configuration;
using System.Data.Common;
using System.Data.Odbc;
using System.Data.OleDb;
//using System.Data.OracleClient;
//↑須另外加入System.Data.OracleClient參考,才能using此命名空間,不過通常很少用OracleClient,所以把它註解掉

namespace SystemDAO
{
        /// <summary>
        /// 此類別為抽象類別,不允許被new,在使用時直接調用即可
        /// </summary>
        public abstract class SqlHelper
        {

            /// <summary>
            /// SqlHelper預設資料庫連線字串
            /// </summary>
            public static readonly string connectionString = ConfigurationManager.ConnectionStrings["dbConn"].ConnectionString.Trim();

            #region 資料提供者
            /// <summary>
            /// 資料提供者,依據目標資料庫不同,須修改不同類型的DbProviderFactory
            /// </summary>
            private static readonly DbProviderFactory dbProviderFactory = SqlClientFactory.Instance;
            //private static readonly DbProviderFactory dbProviderFactory = OdbcFactory.Instance;
            //private static readonly DbProviderFactory dbProviderFactory = OleDbFactory.Instance;
            //private static readonly DbProviderFactory dbProviderFactory = OracleClientFactory.Instance;
            #endregion


            /// <summary>
            /// 為執行命令準備參數
            /// </summary>
            /// <param name="cmd">DbCommand 命令</param>
            /// <param name="conn">資料庫連線</param>
            /// <param name="trans">交易處理</param>
            /// <param name="cmdType">DbCommand類型 (CommandType.StoredProcedure或CommandType.Text)</param>
            /// <param name="cmdText">DbCommand的T-SQL语句 例如:Select * from Products</param>
            /// <param name="cmdParms">使用到的參數集合</param>
            private static void PrepareCommand(DbCommand cmd, DbConnection conn, DbTransaction trans, CommandType cmdType, string cmdText,
              params DbParameter[] cmdParms)
            {
                //判斷資料庫連線狀態
                if (conn.State != ConnectionState.Open) { conn.Open(); }
                //判斷是否需要交易處理
                if (trans != null) { cmd.Transaction = trans; }
                cmd.Connection = conn;
                cmd.CommandText = cmdText;
                cmd.CommandType = cmdType;
                if (cmdParms != null && cmdParms.Length > 0)
                {
                    cmd.Parameters.Clear();//清除參數集合(防呆)
                    cmd.Parameters.AddRange(cmdParms); 
                     
                }
            }

            #region 連線資料庫存取

            #region ExecuteNonQuery 異動資料

            /// <summary>
            /// 執行新增、修改、刪除指令,透過指定連接字串。
            /// </summary>
            /// <param name="connectionString">工程師自行指定DB連線字串</param>
            /// <param name="cmdType">DbCommand類型 (CommandType.StoredProcedure或CommandType.Text)</param>
            /// <param name="cmdText">預存程式名稱 或 T-SQL 语句</param>
            /// <param name="commandParameters">使用到的參數集合</param>
            /// <returns>受影響的資料筆數</returns>
            public static int ExecuteNonQuery(string connectionString, CommandType cmdType, string cmdText, params DbParameter[] commandParameters)
            {
                using (DbConnection conn = dbProviderFactory.CreateConnection())
                {
                    conn.ConnectionString = connectionString;
                    DbCommand cmd = conn.CreateCommand();
                    //通過PrePareCommand方法將參數逐個加入到DbCommand的参數集合中
                    PrepareCommand(cmd, conn, null, cmdType, cmdText, commandParameters);
                    int val = cmd.ExecuteNonQuery();
                    //清空SqlCommand中的参数列表
                    cmd.Parameters.Clear();
                    return val;

                }//end using 

            }

            /// <summary>
            /// 執行新增、修改、刪除指令,使用SqlHelper預設連接字串。
            /// </summary>
            /// <param name="cmdType">DbCommand類型 (CommandType.StoredProcedure或CommandType.Text)</param>
            /// <param name="cmdText">預存程式名稱 或 T-SQL 语句</param>
            /// <param name="commandParameters">使用到的參數集合</param>
            /// <returns>受影響的資料筆數</returns>
            public static int ExecuteNonQuery(CommandType cmdType, string cmdText, params DbParameter[] commandParameters)
            {
                return ExecuteNonQuery(connectionString, cmdType, cmdText, commandParameters);
            }

            /// <summary>
            /// 執行新增、修改、刪除指令,使用SqlHelper預設連接字串、CommandType.Text
            /// </summary>
            /// <param name="cmdText">CommandType.Text的T-Sql語句</param>
            /// <param name="commandParameters">使用到的參數集合</param>
            /// <returns>受影響的資料筆數</returns>
            public static int ExecuteNonQueryText(string cmdText, params DbParameter[] commandParameters)
            {
                return ExecuteNonQuery(CommandType.Text, cmdText, commandParameters);
            }

            #endregion

            #region  ExecuteScalar

            /// <summary>
            /// 取得第一行第一列的資料,通常使用在Select Count(*) From TableName 有聚合函數的Select指令
            /// 工程師自行指定DB連線字串
            /// </summary>
            /// <param name="connectionString">工程師自行指定DB連線字串</param>
            /// <param name="cmdType">DbCommand類型 (CommandType.StoredProcedure或CommandType.Text)</param>
            /// <param name="cmdText">預存程式名稱 或 T-SQL 語句</param>
            /// <param name="commandParameters">使用到的參數集合</param>
            /// <returns>回傳第一行第一列的資料,型別不確定</returns>
            public static object ExecuteScalar(string connectionString, CommandType cmdType, string cmdText, params DbParameter[] commandParameters)
            {
                using (DbConnection conn = dbProviderFactory.CreateConnection())
                {
                    conn.ConnectionString = connectionString;//連線字串
                    DbCommand cmd = conn.CreateCommand();
                    PrepareCommand(cmd, conn, null, cmdType, cmdText, commandParameters);
                    object val = cmd.ExecuteScalar();
                    //為了讓同一份Parameters的參考可以重覆給其他指令使用,所以清除
                    cmd.Parameters.Clear();//清除參數集合 
                    conn.Close();
                    return val;
                }
            }
            /// <summary>
            /// 取得第一行第一列的資料,通常使用在Select Count(*) From TableName 有聚合函數的Select指令
            /// 使用SqlHelper預設DB連線字串
            /// </summary>
            /// <param name="cmdType">DbCommand類型 (CommandType.StoredProcedure或CommandType.Text)</param>
            /// <param name="cmdText">預存程式名稱 或 T-SQL 語句</param>
            /// <param name="commandParameters">使用到的參數集合</param>
            /// <returns>回傳第一行第一列的資料,型別不確定</returns>
            public static object ExecuteScalar(CommandType cmdType, string cmdText, params DbParameter[] commandParameters)
            {
                return ExecuteScalar(connectionString, cmdType, cmdText, commandParameters);
            }


            /// <summary>
            /// 取得第一行第一列的資料,通常使用在Select Count(*) From TableName 有聚合函數的Select指令
            /// 使用SqlHelper預設DB連線字串、CommandType.Text
            /// </summary>
            /// <param name="cmdText">T-SQL 語句</param>
            /// <param name="commandParameters">使用到的參數集合</param>
            /// <returns>回傳第一行第一列的資料,型別不確定</returns>
            public static object ExecuteScalarText(string cmdText, params DbParameter[] commandParameters)
            {
                return ExecuteScalar(connectionString, CommandType.Text, cmdText, commandParameters);
            }

            #endregion

            #region ExecuteReader 
            /// <summary>
            /// 執行Select查詢指令,工程師指定連線字串。
            /// 前端呼叫時記得要用using包住回傳的DbDataReader變數來關閉連線
            /// </summary>
            /// <param name="connectionString">工程師自行指定DB連線字串</param>
            /// <param name="cmdType">DbCommand類型 (CommandType.StoredProcedure或CommandType.Text)</param>
            /// <param name="cmdText">預存程式名稱 或 T-SQL 語句</param>
            /// <param name="commandParameters">使用到的參數集合</param>
            /// <returns>回傳DbDataReader指標</returns>
            public static DbDataReader ExecuteReader(string connectionString, CommandType cmdType, string cmdText, params DbParameter[] commandParameters)
            {

                DbConnection conn = dbProviderFactory.CreateConnection();
                conn.ConnectionString = connectionString;//連線字串
                DbCommand cmd = conn.CreateCommand();
                //↓不寫這行的話,由實作的Provider決定數值,OleDb、Odbc、SqlClient預設30秒,OracleClient為0不逾時
                cmd.CommandTimeout = 0;//執行SQL指令時間,0為不逾時
                try
                {
                    //開啟連線
                    PrepareCommand(cmd, conn, null, cmdType, cmdText, commandParameters);
                    DbDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
                    cmd.Parameters.Clear();//清除參數集合
                    return reader;
                }
                catch (DbException ex)
                {
                    conn.Close();//發生查詢例外就關閉連線
                    throw ex;
                }
            }

            /// <summary>
            /// 執行Select查詢指令,使用SqlHelper預設連線字串。
            /// 前端呼叫時記得要用using包住回傳的DbDataReader變數來關閉連線
            /// </summary>
            /// <param name="cmdType">DbCommand類型 (CommandType.StoredProcedure或CommandType.Text)</param>
            /// <param name="cmdText">預存程式名稱 或 T-SQL 語句</param>
            /// <param name="commandParameters">使用到的參數集合</param>
            /// <returns>回傳DbDataReader指標</returns>
            public static DbDataReader ExecuteReader(CommandType cmdType, string cmdText, params DbParameter[] commandParameters)
            {
                return ExecuteReader(connectionString, cmdType, cmdText, commandParameters);
            }

            /// <summary>
            /// 執行Select查詢指令,使用SqlHelper預設連線字串、CommandType.Text
            /// 前端呼叫時記得要用using包住回傳的DbDataReader變數來關閉連線
            /// </summary>
            /// <param name="cmdText">T-SQL 語句</param>
            /// <param name="commandParameters">使用到的參數集合</param>
            /// <returns>回傳DbDataReader指標</returns>
            public static DbDataReader ExecuteReaderText(string cmdText, params DbParameter[] commandParameters)
            {
                return ExecuteReader(CommandType.Text, cmdText, commandParameters);
            }

            #endregion

            #region 交易
            /// <summary>
            /// 執行多條SQL語句,實現資料庫交易,使用SqlHelper預設DB連線字串
            /// </summary>
            /// <param name="sqlStringList">參數集合</param>
            public static void ExecuteSqlTran(Dictionary<string, DbParameter[]> sqlStringList)
            {
                ExecuteSqlTran(connectionString, CommandType.Text, sqlStringList);
            }
            /// <summary>
            /// 執行多條SQL語句,實現資料庫交易
            /// </summary>
            /// <param name="sqlStringList">多條SQL語句</param>  
            public static void ExecuteSqlTran(string connectionString, CommandType cmdType, Dictionary<string, DbParameter[]> sqlStringList)
            {

                using (DbConnection conn = dbProviderFactory.CreateConnection())
                {
                    conn.ConnectionString = connectionString;//指定連線字串
                    conn.Open();//開啟連線
                    DbTransaction trans = conn.BeginTransaction();//開始交易
                    DbCommand cmd = conn.CreateCommand();
                    try
                    {
                        foreach (KeyValuePair<string, DbParameter[]> item in sqlStringList)
                        {
                            PrepareCommand(cmd, conn, trans, cmdType, item.Key, item.Value);
                            cmd.ExecuteNonQuery();//執行一筆SQL異動語句
                                                  //清空DbCommand中的參數集合
                            cmd.Parameters.Clear();
                        }//end foreach
                        trans.Commit();//交易提交
                    }
                    catch (DbException ex)
                    {
                        trans.Rollback();//交易Rollback
                        throw ex;
                    }
                }
            }

            #endregion

            #endregion


            #region 大量寫入 Modify by Shadow at 2014-08-02

            /// <summary>
            /// 大量批次新增(限對象為Sql Server),使用SqlHelper的預設連線
            /// </summary>
            /// <param name="dtSource">資料來源的DataTable</param>
            /// <param name="destDataTableName">目標資料庫的表格名稱</param>
            /// <param name="optionSqlBulk">匯入時的選項</param>
            public static void SqlBulkCopyFromDataTable(DataTable dtSource, string destDBTableName, SqlBulkCopyOptions optionSqlBulk = SqlBulkCopyOptions.Default)
            {
                SqlBulkCopyFromDataTable(connectionString, dtSource, destDBTableName, optionSqlBulk);

            }

            /// <summary>
            /// 大量批次新增(限對象為Sql Server),工程師自行指定DB連線
            /// </summary>
            /// <param name="connectionString">DB連線字串</param>
            /// <param name="dtSource">資料來源的DataTable</param>
            /// <param name="destDataTableName">目標資料庫的表格名稱</param>
            /// <param name="optionSqlBulk">匯入時的選項</param>
            public static void SqlBulkCopyFromDataTable(string connectionString, DataTable dtSource, string destDBTableName, SqlBulkCopyOptions optionSqlBulk = SqlBulkCopyOptions.Default)
            {

                if (string.IsNullOrEmpty(destDBTableName))
                {
                    throw new Exception("缺少目標資料庫的表格名稱");
                }

                using (SqlConnection conn = new SqlConnection(connectionString))
                {
                    conn.Open();//開啟連線
                                //開始交易
                    SqlTransaction tran = conn.BeginTransaction();
                    //宣告SqlBulkCopy  
                    using (SqlBulkCopy sqlBC = new SqlBulkCopy(conn, optionSqlBulk, tran))
                    {
                        //設定一個批次量寫入多少筆資料       
                        sqlBC.BatchSize = 1000;
                        //設定逾時的秒數        
                        sqlBC.BulkCopyTimeout = 30;
                        //設定要寫入的資料表          
                        sqlBC.DestinationTableName = destDBTableName;
                        foreach (DataColumn dataCol in dtSource.Columns)
                        {
                            //對應資料行         
                            sqlBC.ColumnMappings.Add(dataCol.ColumnName, dataCol.ColumnName);
                        }//end foreach
                         //開始寫入新增 
                        try
                        {
                            sqlBC.WriteToServer(dtSource);
                            tran.Commit();//交易提交
                        }
                        catch (SqlException ex)
                        {
                            tran.Rollback();//交易Rollback
                            throw ex;
                        }

                    }//end using 
                }//end using 

            }
            #endregion



            #region 離線資料庫存取

            #region GetDataTable

            /// <summary>
            /// 取得Select指令回傳的結果集,工程師自行指定DB連線字串
            /// </summary>
            /// <param name="connecttionString">DB連線字串</param>
            /// <param name="cmdTye">DbCommand類型 (CommandType.StoredProcedure或CommandType.Text)</param>
            /// <param name="cmdText">預存程式名稱 或 T-SQL 语句</param>
            /// <param name="commandParameters">使用到的參數集合</param>
            /// <returns>取得Select指令回傳的結果集,型別DataTable</returns>
            public static DataTable GetDataTable(string connectionString, CommandType cmdTye, string cmdText,params DbParameter[] commandParameters)
            {

                DataTable dt = new DataTable();
                using (DbConnection conn = dbProviderFactory.CreateConnection())
                {
                    conn.ConnectionString = connectionString;//連線字串
                    DbCommand cmd = conn.CreateCommand();
                    //↓不寫這行的話,由實作的Provider決定數值,OleDb、Odbc、SqlClient預設30秒,OracleClient為0不逾時
                    cmd.CommandTimeout = 0;//執行SQL指令時間,0為不逾時
                    PrepareCommand(cmd, conn, null, cmdTye, cmdText, commandParameters);
                    DbDataAdapter adapter = dbProviderFactory.CreateDataAdapter();//DbDataAdapter自己會開/關DB連線
                    adapter.SelectCommand = cmd;
                    adapter.Fill(dt);
                    cmd.Parameters.Clear();
                    conn.Close();//自關連線
                }
                return dt;
            }

            /// <summary>
            /// 取得Select指令回傳的結果集,使用SqlHelper預設DB連線字串
            /// </summary>
            /// <param name="cmdTye">DbCommand類型 (CommandType.StoredProcedure或CommandType.Text)</param>
            /// <param name="cmdText">預存程式名稱 或 T-SQL 语句</param>
            /// <param name="commandParameters">使用到的參數集合</param>
            /// <returns>取得Select指令回傳的結果集,型別DataTable</returns>
            public static DataTable GetDataTable(CommandType cmdTye, string cmdText,params DbParameter[] commandParameters)
            {
                return GetDataTable(connectionString, cmdTye, cmdText, commandParameters);
            }

            /// <summary>
            /// 取得Select指令回傳的結果集,使用SqlHelper預設DB連線字串、CommandType.Text
            /// </summary>
            /// <param name="cmdText">T-SQL 语句</param>
            /// <param name="commandParameters">使用到的參數集合</param>
            /// <returns>取得Select指令回傳的結果集,型別DataTable</returns>
            public static DataTable GetDataTableText(string cmdText,params DbParameter[] commandParameters)
            {
                return GetDataTable(CommandType.Text, cmdText, commandParameters);
            }
            #endregion

            #endregion


        }

     
}

編輯SqlHelper.cs檔,還有兩個步驟,請見下圖

這樣前置作業都完成,可以開始使用

2018-12-21 追加GetDataTable Retry抓資料


        /// <summary>
        /// 要重試次數的上限
        /// </summary>
        private static readonly int maxRetryCount = 10;//10次
        /// <summary>
        /// 第二次retry之後要等待的毫秒
        /// </summary>
        private static readonly int waitMilliSeconds = 6000;//6000毫秒
        /// <summary>
        /// 資料庫常常莫名斷線,所以retry抓資料
        /// </summary>
        /// <param name="connecttionString">DB連線字串</param>
        /// <param name="cmdTye">DbCommand類型 (CommandType.StoredProcedure或CommandType.Text)</param>
        /// <param name="cmdText">預存程式名稱 或 T-SQL 語句</param>
        /// <param name="commandParameters">使用到的參數集合</param>
        /// <returns>取得Select指令回傳的結果集,型別DataTable</returns>
        public static DataTable GetDataTable_Retry(string connecttionString, CommandType cmdTye, string cmdText, DbParameter[] commandParameters)
        {
            
            DataTable dt = new DataTable();
            using (DbConnection conn = dbProviderFactory.CreateConnection())
            {
                conn.ConnectionString = connectionString;//連線字串
                DbCommand cmd = conn.CreateCommand();
                cmd.CommandTimeout = 0;

                bool isError = false;//是否錯誤
                int retryCount = 0;//重試次數
                #region 重試DB資料
                do
                {//retry 抓資料
                    retryCount++;
                    if (retryCount>=2)
                    {//重試第2次之後...
                        System.Threading.Thread.Sleep(waitMilliSeconds);
                    }
                    try
                    {
                        PrepareCommand(cmd, conn, null, cmdTye, cmdText, commandParameters);
                        DbDataAdapter adapter = dbProviderFactory.CreateDataAdapter();//DbDataAdapter自己會開/關DB連線
                        adapter.SelectCommand = cmd;
                        adapter.Fill(dt);

                        //成功抓到資料
                        isError = false;//離開retry
                        //break;
                    }//end try
                    catch (Exception ex)
                    {
                        isError = true;//可能進入retry
                        if (retryCount==maxRetryCount)
                        {//達retry上限次數
                            //todo 寫Log
                        }
                    }//end catch
                    finally
                    {
                        if (conn != null && conn.State == ConnectionState.Open)
                        {
                            conn.Close();//自關連線
                        }
                    }
                } while (isError && retryCount<maxRetryCount);//retry超過上限就不再retry
                #endregion
            }//end using
            return dt;
        }

2019-02-26 追加ExecuteScalar Retry抓資料

/// <summary>
/// 要重試次數的上限
/// </summary>
private static readonly int maxRetryCount = 10;//10次
/// <summary>
/// 第二次retry之後要等待的毫秒
/// </summary>
private static readonly int waitMilliSeconds = 6000;//6000毫秒
/// <summary>


public static object ExecuteScalar_Retry(string connectionString, CommandType cmdType, string cmdText, params DbParameter[] commandParameters)
{
  object o = null;//要回傳的物件
  using (DbConnection conn = dbProviderFactory.CreateConnection())
  {
     conn.ConnectionString = connectionString;//連線字串
     DbCommand cmd = conn.CreateCommand();
     cmd.CommandTimeout = 0;

     bool isError = false;//是否錯誤
     int retryCount = 0;//重試次數

     #region 重試DB資料
     do
     {//retry 抓資料
      retryCount++;
      if (retryCount >= 2)
      {//重試第2次之後...
         System.Threading.Thread.Sleep(waitMilliSeconds);
      }
      try
      {
        PrepareCommand(cmd, conn, null, cmdType, cmdText, commandParameters);
        o = cmd.ExecuteScalar(); 
        //成功抓到資料
         isError = false;//離開retry
        //break;
        }//end try
        catch (Exception ex)
        {
         isError = true;//可能進入retry
         if (retryCount == maxRetryCount)
         {//達retry上限次數
          //todo寫Log
            StringBuilder sb_params = new StringBuilder();
            if (commandParameters != null)
            {
              foreach (DbParameter param in commandParameters)
              {
               sb_params.AppendLine($@"ParameterName:{param.ParameterName}  ,  Value:{Convert.ToString(param.Value)}");
              }
            }//end if 
               
        }//end catch
        finally
       {
         if (conn != null && conn.State == ConnectionState.Open)
         {
           conn.Close();//自關連線
         }
        }
       } while ( isError && retryCount < maxRetryCount);//retry超過上限就不再retry
      #endregion 

      return o;
            }
             

        }

 

 

 

 

使用方法

我弄了一份展示用的資料,這樣會比較好理解

記得使用SqlHelper呼叫方法前,要先using以下命名空間

using System.Data.SqlClient;//本文範例使用SQL Server
using SystemDAO;
using System.Data;
using System.Data.Common;

DataTable呼叫方式:無參數

//抓全部資料,由於不是參數化查詢,第二個參數傳null
DataTable dt1 = 
SqlHelper.GetDataTableText(@"Select ID,Name,Seq 
                             From TestTable 
                             Order by Seq ASC", null);

DataTable呼叫方式:參數化查詢

DataTable dt2 = 
SqlHelper.GetDataTableText(@"Select Top 1 ID,Name,Seq 
                             From TestTable 
                             Where ID=@ID And Name=@Name
                             Order by Seq ASC", new SqlParameter[]{
                             new SqlParameter(){ ParameterName="@ID" ,SqlDbType=SqlDbType.BigInt,Value=1},
                             new SqlParameter(){ ParameterName="@Name" ,SqlDbType=SqlDbType.NVarChar,Value="Shadow"}});

↓工作上很常出現的表單查詢


            //以下是常見查詢拼接語句
            string sql = @"Select ID,Name,Seq 
               From TestTable 
               Where 1=1 ";
            long Seq = 10;//要查詢的參數
            string[] Names = { "Apple", "Banana" };//要查詢的參數,來源通常是使用者輸入

            List<SqlParameter> para = new List<SqlParameter>();
            if (Seq != -1)//判斷查詢的值
            {
                sql += " And Seq < @Seq ";
                para.Add(new SqlParameter() { ParameterName = "@Seq", SqlDbType = SqlDbType.BigInt, Value = Seq });
            }
            if (Names != null && Names.Length > 0)//判斷查詢的值
            {
                //準備好各參數
               string[] paraNames =  Names.Select((s,i)=>"@p"+i.ToString()).ToArray();
               for (int i = 0; i < paraNames.Length; i++)
               {
                   para.Add(new SqlParameter() { ParameterName = paraNames[i], SqlDbType = SqlDbType.NVarChar, Value = Names[i]});
               }
               
               string inClause = string.Join(",", paraNames);
                
                //拼接SQL語句
               sql += " And Name in (" + inClause + ") ";


                /* 以上動作相當於↓
                 SqlCommand cmd=new SqlCommand();
                 cmd.CommandText = @"Select ID,Name,Seq 
                                     From TestTable 
                                     Where 1=1 And Name in (@p0,@p1)";
                                    cmd.Parameters["@p0"] = "Apple";
                                    cmd.Parameters["@p1"] = "Banana";
                 */
            }//end if
            //排序
            sql += "Order by Seq ASC";

            DataTable dt3 = SqlHelper.GetDataTableText(sql, para.ToArray());
            //↑執行結果就是把Apple、Banana兩筆資料抓出來
           

為了方便展示預存程序呼叫方式,我建了兩個預存程序

--無參數預存程序
Create Procedure TestPro1
As
Begin
 Select ID,Name,Seq
 From TestTable
 Order by Seq ASC
End
Go
 

--有參數預存程序
Create Procedure TestPro2
(
@ID bigint,
@Name nvarchar(6) OUTPUT
)
As
Begin
 Select ID,Name,Seq
 From TestTable
 Where ID=@ID
 Order by Seq ASC

 Select @Name=Name 
 From TestTable
 Where ID=@ID
 Order by Seq ASC

 Return 0
End

Go

呼叫預存程序,無參數

DataTable dt1 =  SqlHelper.GetDataTable(CommandType.StoredProcedure, "TestPro1", null);

呼叫預存程序,傳入參數並取得OUTPUT參數值和Return Value
※注意預存程序有回傳資料集才用GetDataTable()方法去接,如果預存程序沒有回傳資料集的話,則改用ExecuteNonQuery()方法執行即可

long ID = 1;//查詢主鍵
//輸出參數
SqlParameter OutPutPara = new SqlParameter() 
                              { 
                              ParameterName = "@Name", 
                              SqlDbType=SqlDbType.NVarChar, 
                              Size=6,//OUTPUT參數,不指派Size會掛掉
                              Direction=ParameterDirection.Output,
                              };
//回傳值
SqlParameter returnPara = new SqlParameter()
{
    SqlDbType = SqlDbType.Int,
    Direction = ParameterDirection.ReturnValue,
};

//取得Select資料集
DataTable dt1 = 
SqlHelper.GetDataTable(CommandType.StoredProcedure, "TestPro2", new SqlParameter[]
{   new SqlParameter(){ ParameterName="@ID",SqlDbType=SqlDbType.BigInt,Value=ID},
    OutPutPara,
    returnPara
});
             
//取得輸出參數值:Shadow
string Name =  Convert.ToString(OutPutPara.Value);
//回傳值為:0
int returnValue = Convert.ToInt32(returnPara.Value);       

ExecuteReader()呼叫方式
※跟GetDataTable()同樣在抓Select資料集使用,ExecuteReader()是連線資料庫存取、GetDataTable()是離線資料庫存取,兩者詳細差異就不在此贅述
※我之前有過經驗,使用Odbc Provider存取Informix資料庫,撈資料集時全部使用ExecuteReader()方法,後來似乎Connection Pool連線不夠用,程式當掉
   最後改用GetDataTable()就好了,推測可能原先那種組合撈資料方式並無法完全關閉連線

using (DbDataReader reader = SqlHelper.ExecuteReaderText(@"Select ID,Name,Seq 
                                                           From TestTable
                                                           Where Name Like @Name
                                                           Order by Seq ASC",
       new SqlParameter[]{
       new SqlParameter(){ ParameterName="@Name",SqlDbType=SqlDbType.NVarChar,Value="%"+"a"+"%"}
      //找名字有包含a字串,'%'萬用字元必須是參數裡的值才是正確Like參數化查詢用法
                         }))
      {//start using
         while (reader.Read())
         {
           Console.WriteLine(reader["ID"] + "|" + reader["Name"]);//一筆一筆抓值出來
         }


      }//end using 

ExecuteScalar()呼叫方式:用來取得彙總函式的值

//算出資料表的筆數
long  count = Convert.ToInt64(SqlHelper.ExecuteScalarText(@"Select Count(*) As Count From TestTable", null));

↓ExecuteScalar()也可以執行新增資料再取得新資料的識別值(通常是主鍵)

//取得新增資料的ID主鍵
long ID = 
Convert.ToInt64(
        SqlHelper.ExecuteScalarText(@"Insert into TestTable(Name,Seq) Values (N'Cherry',4); 
        Select SCOPE_IDENTITY()", null));

ExecuteNonQuery()方法主要用在Insert、Update、Delete語句

//新增資料
SqlHelper.ExecuteNonQueryText("Insert into TestTable (Name,Seq) Values (@Name,@Seq)", new SqlParameter[] { 
             new SqlParameter(){ParameterName="@Name",SqlDbType=SqlDbType.NVarChar,Value="Test姓名"},
             new SqlParameter(){ParameterName="@Seq",SqlDbType=SqlDbType.BigInt,Value=7}
            });
//修改資料
SqlHelper.ExecuteNonQueryText("Update TestTable Set Name=@Name Where Seq=7", new SqlParameter[] { 
             new SqlParameter(){ParameterName="@Name",SqlDbType=SqlDbType.NVarChar,Value="更改姓名"}
            });
//刪除資料
SqlHelper.ExecuteNonQueryText("Delete From  TestTable Where Seq=7", null);

ExecuteReader()、ExecuteScalar()、ExecuteNonQuery()這三個方法也可以搭配預存程序使用,請參考上面GetDataTable()的用法

再來要提到的是,只有SQL Server資料庫才支援的功能:批次快速大量新增資料

通常資料來源都從Excel、CSV、TXT...等檔案先抓到DataTable裡儲存

然後再由DataTable直接灌入SQL Server,請見以下用法

   DataTable dtExcel = new DataTable();
   dtExcel.Columns.Add("ID",typeof(long));//欄位名稱需和DB裡的欄位名稱相同
   dtExcel.Columns.Add("Name", typeof(string));//欄位名稱需和DB裡的欄位名稱相同
   dtExcel.Columns.Add("Seq", typeof(long));//欄位名稱需和DB裡的欄位名稱相同

   for (int i = 0; i < 10000; i++) //假裝這是從Excel抓進DataTable的10000筆資料
   {
      DataRow dr =  dtExcel.NewRow();
      dr["Name"] = "Name" + i.ToString().Substring(0,1);//姓名都給5個長度
      dr["Seq"] = i;
      dtExcel.Rows.Add(dr);
   }

   //批次大量新增資料
   SqlHelper.SqlBulkCopyFromDataTable(dtExcel, "TestTable", SqlBulkCopyOptions.Default);
           
            

最後是交易,基本用法↓

//先建立一個SQL語句參數用的集合
Dictionary<string, DbParameter[]> sqlPara = new Dictionary<string, DbParameter[]>();
sqlPara.Add("Insert into TestTable (Name ,Seq) Values (@Name,@Seq)", 
             new SqlParameter[]{ 
             new SqlParameter(){ ParameterName="@Name",SqlDbType=SqlDbType.NVarChar,Value="測試姓名"},            
             new SqlParameter(){ ParameterName="@Seq",SqlDbType=SqlDbType.BigInt,Value=78}});

sqlPara.Add("Update TestTable Set Name=@Name Where Seq=78", 
             new SqlParameter[]{ 
             new SqlParameter(){ ParameterName="@Name",SqlDbType=SqlDbType.NVarChar,Value="來測姓名"}});

            //由於Dictionary關係,每條SQL語句必須不一樣,因為Dictionary的Key不能重覆,其實加入SQL語句不重覆也很簡單,若遇到相同SQL的話,多打幾個空格就好,如下示範
sqlPara.Add("Insert into TestTable (Name ,Seq) Values (@Name,@Seq)    ", 
             new SqlParameter[]{ 
             new SqlParameter(){ ParameterName="@Name",SqlDbType=SqlDbType.NVarChar,Value="LastNa"},           
             new SqlParameter(){ ParameterName="@Seq",SqlDbType=SqlDbType.BigInt,Value=99}});

            //執行交易
            SqlHelper.ExecuteSqlTran(sqlPara);

交易過程中若發生例外情況,全部資料會Rollback,都不會異動到資料表

//先建立一個SQL語句參數用的集合
Dictionary<string, DbParameter[]> sqlPara = new Dictionary<string, DbParameter[]>();
sqlPara.Add("Insert into TestTable (Name ,Seq) Values (@Name,@Seq)", 
             new SqlParameter[]{ 
             new SqlParameter(){ ParameterName="@Name",SqlDbType=SqlDbType.NVarChar,Value="測試姓名"},
             new SqlParameter(){ ParameterName="@Seq",SqlDbType=SqlDbType.BigInt,Value=78}});

//Name欄位不允許NULL
sqlPara.Add("Update TestTable Set Name=@Name Where Seq=78", 
             new SqlParameter[]{ 
             new SqlParameter(){ ParameterName="@Name",SqlDbType=SqlDbType.NVarChar,Value=DBNull.Value}});

            try
            {
                //執行交易,資料會Rollback,第一筆資料不會被塞入到DB
                SqlHelper.ExecuteSqlTran(sqlPara);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }

如果交易有使用到取得識別值函式,例如SCOPE_IDENTITY()的話,得注意它的SQL語句要放在Dictionary的同一Key值裡

Dictionary<string, DbParameter[]> sqlPara = new Dictionary<string, DbParameter[]>();
//SCOPE_IDENTITY()放在同一道命令裡
sqlPara.Add(@"Insert into TestTable (Name ,Seq) Values (@Name1,@Seq) 
              Insert into TestTable (Name ,Seq) Values (@Name2,SCOPE_IDENTITY())",
              new SqlParameter[]{ 
              new SqlParameter(){ ParameterName="@Name1",SqlDbType=SqlDbType.NVarChar,Value="測試姓名"},
              new SqlParameter(){ ParameterName="@Seq",SqlDbType=SqlDbType.BigInt,Value=44},
              new SqlParameter(){ ParameterName="@Name2",SqlDbType=SqlDbType.NVarChar,Value="測試姓名2"}});

            //執行交易
            SqlHelper.ExecuteSqlTran(sqlPara);
/*以下是SCOPE_IDENTITY()錯誤用法*/
Dictionary<string, DbParameter[]> sqlPara = new Dictionary<string, DbParameter[]>();
//拆成兩道命令的話,SCOPE_IDENTITY()會是NULL
sqlPara.Add("Insert into TestTable (Name ,Seq) Values (@Name,@Seq)", 
             new SqlParameter[]{ 
             new SqlParameter(){ ParameterName="@Name",SqlDbType=SqlDbType.NVarChar,Value="測試姓名"},
             new SqlParameter(){ ParameterName="@Seq",SqlDbType=SqlDbType.BigInt,Value=44}});

sqlPara.Add("Insert into TestTable (Name ,Seq) Values (@Name,SCOPE_IDENTITY())", 
             new SqlParameter[]{ 
             new SqlParameter(){ ParameterName="@Name",SqlDbType=SqlDbType.NVarChar,Value="測試姓名2"}
         });
            //交易Rollback,Seq資料行不允許NULL
            SqlHelper.ExecuteSqlTran(sqlPara); 

※ 2019-02-15追記:

現在才發覺資料庫交易其實不用包裝成ExecuteSqlTran()方法,不過Sample Code多寫就算了XD

只要呼叫ExecuteNonQuery()、ExecuteScalar()再搭配此篇文章 [C#] EntityFramework交易寫法 Sample Code 介紹的 TransactionScope物件即可

交易都成功就執行scope.Complete(),否則不執行scope.Complete(),同樣達到Commit、Rollback效果 

 

留意SQL Injection

提醒一件事,如果查詢條件的參數,來自外部輸入(Url的QueryString)或使用者輸入(填表單欄位)

最好使用參數化查詢來執行SQL語法,以避免發生SQL Injection被駭客搞掉資料

以下就是未使用參數化查詢的錯誤示範

string Name="Shadow";//假裝此值是外部輸入
//字串拼接SQL容易發生SQL Injection問題
DataTable dt =  
SqlHelper.GetDataTableText("Select ID,Name,Seq From TestTable Where Name='" + Name + "' Order by Seq ASC", null);

//只會顯示一筆資料
foreach (DataRow dr in dt.Rows)
{
   Console.WriteLine("ok:" + dr["ID"] + "|" + dr["Name"]);
}

Name = "' Or '1'='1";//有心人亂輸入
dt = 
SqlHelper.GetDataTableText("Select ID,Name,Seq From TestTable Where Name='" + Name + "' Order by Seq ASC", null);
//顯示全部資料
foreach (DataRow dr in dt.Rows)
{
   Console.WriteLine("SQL Injection:" + dr["ID"] + "|" + dr["Name"]);
}


Name = @"'; Declare @Table_Name nvarchar(4000);
       SELECT Top 1 @Table_Name = TABLE_NAME FROM information_schema.tables;
       Exec('Delete From '+@Table_Name);  --";//有心人亂輸入,刪除資料表全部資料
dt = SqlHelper.GetDataTableText("Select ID,Name,Seq from TestTable Where Name='" + Name + "' Order by Seq ASC", null);
        

還有一種情況,雖然使用參數化查詢,但本質上還是在拼接字串,所以仍然有SQL Injection問題,最常見在預存程序的查詢條件拼接


--參數拼接字串的錯誤示範
CREATE Procedure [dbo].[TestProcedure]
(
 @Name nvarchar(6),
 @Seq bigint
)
As
Begin

Declare @sql varchar(4000)
Set @sql='Select ID,Name,Seq
          From TestTable
          Where 1=1 '

		 
IF IsNull(@Name,'')<>''
Begin
 set @sql = @sql +' And Name Like  ''%'+@Name+'%''  '

End

IF (@Seq Is Not Null)
Begin

set @sql = @sql +' And Seq < '+ Convert(varchar,@Seq)
End 

set @sql = @sql +' Order by Seq ASC'
Exec(@sql)--執行SQL字串

End


GO

執行結果

以程式呼叫產生的SQL Injection

所以,想要撰寫純T-SQL來實作查詢條件篩選(例如在預存程序裡),以下提供三種正確無SQL Injection風險的範例

Declare @ID bigint =NULL --給NULL或-1,相當於略過ID查詢條件
Declare @Seq bigint =5 --給NULL或-1,相當於略過Seq查詢條件
Declare @Name nvarchar(10) = '%Sha%' --'%%'萬用字元必須是參數值,而不要在SQL語句中拼接'%'萬用字元
--@Name給NULL或空字串,相當於略過Name查詢條件

--第一種,由於使用Or,查詢效能可能不大好
Select ID,Name,Seq from TestTable
Where ( Seq< @Seq Or  IsNULL(@Seq,-1)=-1)
      And   
	  ( Name Like @Name Or IsNull(@Name,'')='')
	  And 
	  ( ID = @ID Or IsNull(@ID,-1)=-1)

--第二種,和第三種概念類似
SELECT  ID,Name,Seq 
FROM TestTable
WHERE seq < IIF(IsNull(@Seq,-1) <>-1,@Seq,100000)
And Name Like IIF(isNUll(@Name,'') <>'',@Name,Name)
And ID = IIF(isNUll(@ID,-1) <>-1,@ID,ID)

--第三種
SELECT  ID,Name,Seq 
FROM TestTable
Where Seq <  CASE WHEN IsNull(@Seq,-1) <>-1 THEN @Seq ELSE 100000 END --當Seq為-1或Null時就給100000,相當於撈全部資料
AND	Name Like CASE WHEN isNUll(@Name,'') <>'' THEN @Name ELSE Name END --當@Name為Null或空字串時,Name=Name,相當於撈全部資料
AND	ID = CASE WHEN isNUll(@ID,-1) <>-1 THEN @ID ELSE ID END --當@ID為Null或-1時,ID=ID,相當於撈全部資料






/***※補充:SQL In查詢語句和時間區間的查詢範例***/

Declare @Param nvarchar(4000)  = 'Shadow,Sharon'
Declare @StartDate Datetime ='2015-12-01'
Declare @EndDate Datetime='2015-12-09'
--第一種寫法,我已經事先在資料表新增名為InsertDatetime時間欄位
Select ID,Name,Seq 
from TestTable
where (IsNull(@Param,'')=''  Or  CharIndex(Name,@Param)>0)
And ( (@StartDate Is NULL And @EndDate Is NULL) Or (InsertDatetime Between @StartDate And @EndDate))


--第二種寫法
Select ID,Name,Seq  
from TestTable
Where 
 @Param Like IIF(isNUll(@Param,'') <>'','%'+Name+'%',@Param)
 And 
( InsertDatetime>= IIF((@StartDate Is NULL And @EndDate Is NULL),InsertDatetime,@StartDate) 
  And InsertDatetime<=IIF((@StartDate Is NULL And @EndDate Is NULL),InsertDatetime,@EndDate) )

結語

從頭看下來,我好像把ADO.net技術全部講完了XD

此篇文章從早上9點寫到隔天凌晨2點,我也太熱血XDD