提前说明:正如网友反映的一样,为了不至于产生明显的误导,特别加了此首段说明 SQLHelper,几乎是每个过来者必经的阶段,写好一个SQLHelper是非常重要的一环,所以希望年轻的来者,要多加实践,别只看不动手,哪怕照着写一写,也是相当的有益。 对于本框架系列,希望年轻来者在掌握使用的同时,动手照着系列文章写一写,如果照着写出来的,相信成长不是一点半点的;别光看不练,最后只能忽悠却动不了手。
这篇文章很不好写,我在电脑前思索了一天,也不知怎么下手。 关于SQLHelper的文章遍地都是,写的不咋的随时被拍砖,不写吧,本系列又不完整,所以,买了个保险之后,低调点写了。
从哪写起呢?直接把整个SQLHelper类复制一下,文章就算写完了?好像其它遍地都是的文章都差不多是这个样子的。
在还没写完这篇时,曾经有那么个热心人士反编绎过我的框架,还洒了点代码出来了,提前爆光了一下: 详见: 1:CYQ.Data 轻量数据层之路 华丽升级 V1.3出世(五) 2:CYQ.Data 轻量数据层之路 应用示例二 在线聊天(六)
本文停了一天没动了,现在重新执笔动手了,想了想,于是在博客园搜了一下,看了第一页搜出来的10篇SQLHelper相关代码, 简略看了一眼,发现还是鄙人的简洁友好的多,于是,继续写下来了:
其实我们要的SQLHelper很简单,只要能执行下sql语句和存储过程,也就这个样了,至于事务,这里先放一边了。 接着一步一脚印: 1:我们新增加一个SQHelper类,由于本类并不对外开放,所以我们不改修饰符为public,默认就好了
2:由于我们不做成静态方法调用方式,所以我们需要实例化,添加两个构造函数 /// <summary> /// SQLHelper by 路过秋天 /// </summary> class SQLHelper { /// <summary> /// 默认配置连接字符串名:Conn /// </summary> public SQLHelper() { } /// <summary> /// 可以传链接字符串 /// </summary> public SQLHelper(string conn) { } }
3:既然要实例化才调用,那我们只需要一个Command和一个链接就可以了,所以,我们把它们拿到外面定义成全局变量 class SQLHelper { private SqlCommand com = new SqlCommand(); private SqlConnection _con = null; /// <summary> /// 默认配置连接字符串名:Conn /// </summary> public SQLHelper() { } /// <summary> /// 可以传链接字符串 /// </summary> public SQLHelper(string conn) { } } 上面没有直接new 出SqlConnection,是因为它和链接字符串相关,留到构造函数里初始化了。
4:实现构造函数,初始化SqlConnection public SQLHelper() { if (ConfigurationManager.ConnectionStrings["Conn"] != null) { _con = new SqlConnection(ConfigurationManager.ConnectionStrings["Conn"].ConnectionString); com.Connection = _con; } } public SQLHelper(string conn) { if (conn.Length < 25) { conn = ConfigurationManager.ConnectionStrings[conn].ConnectionString; } _con = new SqlConnection(conn); com.Connection = _con; } 默认用webconfig配置Conn,同时上面根据传入的长度,来判断是从配置文件传入,还是直接的链接字符串!
5:我们增加一个全局的成员属性,一个是否记录异常,如果不记录则会抛出异常 public bool WriteLog = true;
6:我这里另开一个Log类,来处理异常,先留下一个静态空方法,回头处理
7:要执行SQL语句或存储过程,免不了要打开和关闭链接,这里封装成方法,加try private void OpenCon() { try { if (_con.State == ConnectionState.Closed) { _con.Open(); } } catch (SqlException err) { if (WriteLog) { Log.WriteLog(err.Message); } } } private void CloseCon() { try { if (_con.State == ConnectionState.Open) { _con.Close(); } } catch (SqlException err) { if (WriteLog) { Log.WriteLog(err.Message); } } } 我们在打开和关闭异时,调用了日志记录功能。
8:我们继承IDisposable接口,完成资源释放 class SQLHelper:IDisposable { //...省略N行... #region IDisposable 成员 public void Dispose() { if (_con != null) { CloseCon(); _con = null; } if (com != null) { com = null; } } #endregion }
9:我们封装一下SqlCommand的几个执行返回 internal int ExeNonQuery(string procName, bool isProc) { //更新删除操作,返回受影响行数 } internal object ExeScalar(string procName, bool isProc) { //返回首行首列的单个记录 } internal SqlDataReader ExeDataReader(string procName, bool isProc) { //返回读取流 } internal DataTable ExeDataTable(string procName, bool isProc) { //返回DataTable,本框架没用到,因为有了MDataTable }
由于存储过程和单独的sql语句混在一起,我加了一个函数来处理这些共同的事情: private void SetCommandText(string commandText, bool isProc) { com.CommandText = commandText; com.CommandType = isProc CommandType.StoredProcedure : CommandType.Text; if (!com.Parameters.Contains("ReturnValue")) { com.Parameters.Add("ReturnValue", SqlDbType.Int).Direction = ParameterDirection.ReturnValue; } } 后面附加了一个常用的返回值参数ReturnValue。
10:实现封装的几个方法,异常则记录日志/抛出 internal int ExeNonQuery(string procName, bool isProc) { //更新删除操作,返回受影响行数 SetCommandText(procName, isProc); int rowCount = 1; try { OpenCon(); com.ExecuteNonQuery(); } catch (SqlException err) { rowCount = 0; if (WriteLog) { Log.WriteLog(err.Message); } } return rowCount; } internal object ExeScalar(string procName, bool isProc) { //返回首行首列的单个记录 SetCommandText(procName, isProc); object returnValue = null; try { OpenCon(); returnValue = com.ExecuteScalar(); } catch (SqlException err) { if (WriteLog) { Log.WriteLog(err.Message); } } return returnValue; } internal SqlDataReader ExeDataReader(string procName, bool isProc) { //返回读取流 SetCommandText(procName, isProc); SqlDataReader sdr = null; try { OpenCon(); sdr = com.ExecuteReader(CommandBehavior.CloseConnection); if (sdr != null && !sdr.HasRows) { sdr.Close(); sdr = null; } } catch (SqlException err) { if (WriteLog) { Log.WriteLog(err.Message); } } return sdr; } internal DataTable ExeDataTable(string procName, bool isProc) { //返回DataTable,本框架没用到,因为有了MDataTable SetCommandText(procName, isProc); SqlDataAdapter sdr = new SqlDataAdapter(com); DataTable dataTable = new DataTable(); try { OpenCon(); sdr.Fill(dataTable); } catch (SqlException err) { if (WriteLog) { Log.WriteLog(err.Message); } } finally { sdr.Dispose(); } return dataTable; }
11:参数方法,无论是存储过程,还是传参型的sql语句,都要用到 A:参数增加: internal void AddParameters(string parameterName, object value) { if (!com.Parameters.Contains(parameterName)) { com.Parameters.AddWithValue(parameterName, value); } } internal void AddParameters(string parameterName, object value, SqlDbType sqlDbType) { if (!com.Parameters.Contains(parameterName)) { com.Parameters.Add(parameterName, sqlDbType).Value = value; } }
B:参数清除: internal void ClearParameters() { if (com != null && com.Parameters != null) { com.Parameters.Clear(); } }
12:返回值属性,一般用于返回记录总数 private int returnValue; public int ReturnValue { get { if (com != null && com.Parameters != null) { int.TryParse(Convert.ToString(com.Parameters["ReturnValue"].Value),out returnValue); } return returnValue; } set { returnValue = value; } } 就此,SQLHelper 就算写完了,余下要处理一下日志记录与异常抛出。
13:Log类异常日志记录与抛出 class Log { public static void WriteLog(string message) { if (IsCanWrite()) { InsertLogToData(message); } else { throw new Exception(message); } } private static bool IsCanWrite() { //从配置文件取 } private static void InsertLogToData(string message) { //错误日志入库,可以自定义写文本或入数据库 } } 判断是一下是否设置为可写日志,如果是则写,否则抛异常。
14:实现IsCanWrite方法 private static bool IsCanWrite() { bool IsCanWriteLog; bool.TryParse(Convert.ToString(ConfigurationManager.AppSettings["IsWriteLog"]), out IsCanWriteLog); return IsCanWriteLog; } 从配置文件里取配置,如果配置为true,就记录日志,否则抛出异常。
插曲:写到这里,博客园又挂了,好在有预感之前保存了一下文章:上图: 十几分钟后,正常了!!!
15:实现InsertLogToData方法,将错误异常入库 private static void InsertLogToData(string message) { //错误日志入库,可以自定义写文本或入数据库 if (ConfigurationManager.ConnectionStrings["LogConn"] == null) { return ; } string pageUrl = System.Web.HttpContext.Current.Request.Url.ToString(); SQLHelper helper = new SQLHelper("LogConn"); helper.WriteLog = false;//再产生错误就不写日志了,不能产生死循环 try { helper.AddParameters("@PageUrl", pageUrl, System.Data.SqlDbType.NVarChar); helper.AddParameters("@ErrorMessage", message, System.Data.SqlDbType.NVarChar); helper.ExeNonQuery("insert into ErrorLogs(PageUrl,ErrorMessage) values(@PageUrl,@ErrorMessage)", false); helper.Dispose(); } catch { //啥也不做了 } } 这里只是对ErrorLogs表进行插入数据操作,将当前发生错误的Url地址及错误信息插入表中。 同时也演示了一把SQLHelper的用法。当然你可以修改成自己需要的日志记录方法。
至此,想了一天,终于把这个SQLHelper类给写完了,虽然此类在本框架中不对外开放使用,不过有心的读者仍可以独立出去使用。
OK,本节也就到此结束了。欢迎读者留言讨论或提出建议!
后语:从你学会写或用SQLHelper那时起,你还会去界面写一堆的类似这样的代码吗? SqlConnection con = new SqlConnection("server=.;database=CYQ;uid=sa;pwd=123456"); SqlCommand com = new SqlCommand(); com.Connection = con; com.CommandType = CommandType.Text; com.CommandText = "SELECT * FROM CYQTABLE WHERE ID=888"; con.Open(); SqlDataReader sdr=com.ExecuteReader(); if (sdr != null) { while (sdr.HasRows) { sdr.Read(); //...循环读... } sdr.Close(); } con.Close();
难了吧,回头已太难了,不管是基于认知度的提升还是开发效率上,你都基本不回头了吧。
如果有一天 你又认知了微软的ADO.NET Entity Framework或是NHibernate或其它框架。 又或者有一天 你原创框架了 又或是使用本框架了 你还会回头用那个曾经过的SQLHelper么 只能说,一切回头太难。
备注:完整框架源码会在本系列结束之后另开章节发布,暂时望勿激动,学习思想才是重要的。 分类: CYQ.Data 框架系列
发表评论
如果有一天
你又认知了微软的ADO.NET Entity Framework或是NHibernate或其它框架。 又或者有一天 你原创框架了 又或是使用本框架了 你还会回头用那个曾经过的SQLHelper么 只能说,一切回头太难。 =============== 不存在什么回头不回头的问题。 如果从头到尾,从一开始到将来,都是自己写(代码/框架)的话,那么就没有什么回不回头的问题。 因为以前写的,是现在的一个基石。 ADO.NET Entity Framework 内部就没有类似SQLHelp的东东吗? 至少会有ADO.net吧。 至于为什么给我们的感觉,调用的方法不一样了? 那是因为不是我们自己写的,不是我们自己升级的。 昨天看了看没有人回复,今天居然有这么多人回复了,恭喜楼主。
今天又仔细看了楼主的博文,结合大家的回复,我觉得有两个方面需要注意: 1,全局变量的使用,比如你的com对象,虽然理论上面说每次使用SQLHelper的时候new一个出来,但很多开发人员没有这个习惯,喜欢new一个以后一直用着,这样在多线程中很容易出现问题,我们公司曾经出现过这样的问题; 2,对事务的处理,实际上一个数据访问对象功能是否强大关键要看事务处理能力了,比如先New一个SQLHelper,一直用着,一部分代码没有使用事务,一部分使用了事务,就很可能出现冲突,另外,如果你有多个业务组件来调用你的SQLHelper,那么这些组件之间的事务协调是很麻烦的。 线程安全和事务处理是企业级数据组件重点关注的问题! 实际上,如果采用良好的设计模式(最简单的就是工厂模式),那么扩展支持多种数据库非常简单,例如PDF.NET数据开发框架对SQLite数据访问的扩展:
@
路过秋天
既然你说了句不好听的 我就让你明白明白 首先, 就从你的helper类说起, 首先说说什么叫helper类. Helper类是组合了一些方法的类, 这些方法跟应用程序的逻辑无关. 在MVC框架里我们见过HtmlHelpers. 通常, Helper类的方法大多数是静态的, 因为在一般情况下helper类不需要扩展. 也就是说当且仅当helper类需要扩展的时候才将它设计成可实例化的形式. 你的helper类扩展了吗? EnterpriseLibrary里的helper类是静态化的, 你为啥偏偏出一个可实例化的? 从哪能解释的通? 关于helper类的讨论: http://blogs./b/nickmalik/archive/2005/09/06/461404.aspx http://blogs./b/nickmalik/archive/2005/09/06/461404.aspx 其次, 说说你的命名. 按照C#的命名标准, 应该是Sql而不是SQL. 这点在.NET类库里已经体现. 还有你程序里conn, Con不分, 这又是何意? <代码大全>里曾经说过缩写一个单词的准则, 一般来讲只有字母少于一半的缩写才值得缩写, connetion -> conn 可以, 而且值得, conn -> con 不知道你是为何. 微软的命名规范 有时间看看 http://msdn.microsoft.com/en-us/library/czefa0ke%28vs.71%29.aspx 第三, IsCanWrite和InsertLogToData这两个方法的命名有问题, 说明你英文水平不是很好. 先前看过alibaba的一份关于html页面class/id的命名规则, 能用英文的尽量用英文, 但是必须地道, 否则请用拼音. Is/Can 这两个词放一起 够让人笑掉大牙的了吧, InsertLogToDb貌似比ToData更好些吧. 第四: 楼上有人劝你用using,这是语言赋予你的自动清除垃圾的简洁优雅的语法, Effective C#里不止一次强调要尽量用这种语法而不是自己去Dispose, 你怎么就是不听? 不是误导新人是什么? 还有就是楼上诸君提到的, 这里恕不赘述. 我说你, 不是为了争什么, 也不是彰显自己水平有多高. 就是看不太管你这种半瓶子咣当的就忙不及出来写文章, 非要写, 可以在家自己写, 别往网上发, 否则误人子弟的责任你担得起么! 如果有要讨论的欢迎加我QQ:66074915. 并无敌意, 不打不相识 @
[兴华]
问题1:Helper类的方法大多数是静态的,是,很网上有很多是静态的,可是这规定是死的?谁家定死一定要静态,不静态就误人了?我的类扩展了吗?你看过这个类在框架的里的使用了?没看赶紧看了再说话。 问题2:C#命名没有标准,只有规范,只是参考的规范。我整体规范偏离一般的规范很远么?你就这么纠住一两个词说个天大的事一样? 问题3:方法起名我起的没地道的英文怎么了?妨碍到大家的阅读与理解了么?这么多个看贴的,几个像你这样笑掉大牙的? 问题4:用using,最终的结果还是调用Disponse方法,再说本类也支持这种写法,我提醒大伙手动调用链接关闭咋了?有多人新手懂的using的原理,老是这么用只会造成是也这么用,不是也这么用。 最后总结一下:你有点像读死书的人,不是考了研还是读了博吧? 如果讨论,欢迎继续,并无敌意,只是坚持下自己的论点。 |
|