1 引言 在近期使用.NET开发项目时,发现这样一些问题,影响着程序的复用性和软件的灵活性: (1)数据库类型是变动的 原因有两个:一是因为不同的客户,对同一个应用程序的性能要求或费用开销不同,要求使用的数据库类型不同;二是我们在开发程序时,在不同的使用环境下,需要使用不同类型的数据库。 比如,如果使用ACCESS数据库,那么在程序中会使用System.Data.OleDb命名空间下的类,如OleDbConnection,OleDbParameter,OleDbDataAdapter等等。如果现在要求支持SQL Server数据库该怎么办呢?那么就需要修改现有的大量的代码,将System.Data.OleDb下的类全部替换为System.Data.SqlClient命名空间下的类,即使是使用“查找/替换”进行批量的修改,也是非常繁琐,也很容易出错。当然还会存在一些错误,比如,OleDb是使用“?”来传递参数的,而SQL Server中使用的是命名参数(如,“@para”)。因此还需要修改大量的SQL语句,麻烦啊! (2)主键生成的规则不同 不同应用环境下,可能使用不同的主键生成策略,有的需要使用数据库唯一键,有的使用表唯一键,有的需要加入时间或其他标记,有的只使用简单的计数器就可以,有的使用GUID…… 如果程序中写死了,下次环境变了,需要使用新的规则时,就又得改代码喽。 (3)数据库连接管理 控制连接的数目,有时时单个连接,有时是若干个连接,有时使用连接池…… (4)SQL语句安全性检查 为了防止恶意攻击,需要对SQL进行检查。有很多种检查方法,究竟使用哪个呢? (5)程序中SQL语句到处飞 如果使用关系数据库,SQL语句是少不了的。但是SQL语句在应用层出现太多,一旦数据库发生变化,那么上层需要修改的SQL语句就太多了,而且在编译时发现不了SQL语句的错误,在运行时才能找到。另外大量的SQL语句,还严重影响了程序的美观。 对于上面的问题,最好的解决方法就是建立一个数据库访问中间层,来屏蔽这些问题。(当然有一些现成的框架,如NHibernate,可以使用,不需要我们自己动手。)本文后续部分将讨论如何解决这些问题,主要在如何设计,并给出部分实现。 2 约定 (1)本文中谈及的数据库仅限于关系数据库。数据库类型指不同的关系数据库系统,如Oracle,SQL Server,Sybase等等。 (2)数据库对象指ADO.NET中访问数据库的对象:Connection对象,Command对象,Adapter对象、Parameter对象。 3 应用程序的一般结构 通常,数据库相关的应用程序应该具有图1所示的结构。至于为什么,就不用多说了。从图中可以看到数据库访问层所处的位置,及其应该具有的功能。
图1 应用程序一般结构 4 适应不同类型的数据库 如第1节所述,在ADO.NET中,使用不同类型的数据库,主要就是这些数据库对象的变化:Connection,Command,Adapter,Parameter。在程序中我们需要根据不同的数据库创建合适的对象。比如,使用SQL Server数据库,就需要引用System.Data.SqlClient命名空间,并使用SqlConnection, SqlCommand, SqlAdapter对象;如果使用ORACLE数据库,则要引用System.Data.OracleClient命名空间,并使用OracleConnection,OralceCommand等等。 通常的简单做法是使用一个数据库对象工厂,在工厂中通过条件判断,是何种类型的数据库,创建相应的数据库对象,代码如下: [代码1] 简单,但缺乏弹性和复用性的方法 DbObjectFactory(Simple)
1 /// <summary> 2 /// 数据库类型。 3 /// </summary> 4 public enum DbType 5 { 6 SqlServer, 7 Oracle, 8 OleDb, 9 } 10 11 public class DbObjectFactory 12 { 13 /// <summary> 14 /// Connection 对象的创建。 15 /// </summary> 16 /// <param name="dbType">数据库类型。</param> 17 /// <returns>Connection 对象。</returns> 18 public static IDbConnection CreateConnection(DbType dbType) 19 { 20 switch(dbType) 21 { 22 case DbType.SqlServer: 23 return new SqlConnection(); 24 case DbType.Oracle: 25 return new OracleConnection(); 26 case DbType.OleDb: 27 return new OleDbConnection(); 28 } 29 throw new Exception("不支持的数据库类型。"); 30 } 31 32 /// <summary> 33 /// Parameter 对象的创建。 34 /// </summary> 35 /// <param name="dbType">数据库类型。</param> 36 /// <returns>Parameter 对象。</returns> 37 public static IDataParameter CreateParameter(DbType dbType) 38 { 39 switch(dbType) 40 { 41 case DbType.SqlServer: 42 return new SqlParameter(); 43 case DbType.Oracle: 44 return new OracleParameter(); 45 case DbType.OleDb: 46 return new OleDbParameter(); 47 } 48 throw new Exception("不支持的数据库类型。"); 49 } 50 } 51 上面的代码中都是条件判断,而且 CreateConnection函数和CreateParameter函数代码几乎一样,DataAdapter和Command的创建也是如此。这样的做法缺点主要有两点:(1)代码重复,可维护性差; (2)新增加一种数据库类型时,需要修改现有的代码。 其实我们仔细分析以下数据库类型和对象之间的关系,可以发现图2所示的规律:
很明显,采用抽象工厂模式可以很好的处理不同数据库和不同数据库对象之间的组和系列的关系。如图3:
根据上面的类图,生成C#代码如下:
DbObjectFactory
1 /// <summary> 2 /// 数据库对象工厂的虚基类。 3 /// </summary> 4 public abstract class DbObjectFactory 5 { 6 /// <summary> 7 /// 创建数据库连接对象,并打开连接。 8 /// </summary> 9 /// <returns> 数据库连接。返回null 表示连接失败。</returns> 10 public abstract IDbConnection CreateConnection(); 11 12 /// <summary> 13 /// 创建Command 对象。 14 /// </summary> 15 /// <returns> 执行SQL 的Command 对象。</returns> 16 public abstract IDbCommand CreateCommand(); 17 18 /// <summary> 19 /// 创建DbDataAdapter 对象。 20 /// </summary> 21 /// <returns>DbDataAdapter 对象。</returns> 22 public abstract IDbDataAdapter CreateDataAdapter(); 23 24 /// <summary> 25 /// 创建Parameter 对象。 26 /// </summary> 27 /// <returns>Parameter 对象。</returns> 28 public abstract IDataParameter CreateParameter(); 29 } 30
OleDbObjectFactory
1 /// <summary> 2 /// OleDb 数据库对象工厂。 3 /// </summary> 4 public class OleDbObjectFactory : DbObjectFactory 5 { 6 /// <summary> 7 /// 创建数据库连接对象,并打开连接。 8 /// </summary> 9 /// <returns> 数据库连接。返回null 表示连接失败。</returns> 10 public override IDbConnection CreateConnection() 11 { 12 return new OleDbConnection(); 13 } 14 15 /// <summary> 16 /// 创建Command 对象。 17 /// </summary> 18 /// <returns> 执行SQL 的Command 对象。</returns> 19 public override IDbCommand CreateCommand() 20 { 21 return new OleDbCommand(); 22 } 23 24 /// <summary> 25 /// 创建DbDataAdapter 对象。 26 /// </summary> 27 /// <returns>DbDataAdapter 对象。</returns> 28 public override IDbDataAdapter CreateDataAdapter() 29 { 30 return new OleDbDataAdapter(); 31 } 32 33 /// <summary> 34 /// 创建Parameter 对象。 35 /// </summary> 36 /// <returns>Parameter 对象。</returns> 37 public override IDataParameter CreateParameter() 38 { 39 return new OleDbParameter(); 40 } 41 } 42
OleDbObjectFactory
1 /// <summary> 2 /// Sql Server 数据库对象工厂。 3 /// </summary> 4 public class SqlObjectFactory : DbObjectFactory 5 { 6 /// <summary> 7 /// 创建数据库连接对象,并打开连接。 8 /// </summary> 9 /// <returns> 数据库连接。返回null 表示连接失败。</returns> 10 public override IDbConnection CreateConnection() 11 { 12 return new SqlConnection(); 13 } 14 15 /// <summary> 16 /// 创建Command 对象。 17 /// </summary> 18 /// <returns> 执行SQL 的Command 对象。</returns> 19 public override IDbCommand CreateCommand() 20 { 21 return new SqlCommand(); 22 } 23 24 /// <summary> 25 /// 创建DbDataAdapter 对象。 26 /// </summary> 27 /// <returns>DbDataAdapter 对象。</returns> 28 public override IDbDataAdapter CreateDataAdapter() 29 { 30 return new SqlDataAdapter(); 31 } 32 33 /// <summary> 34 /// 创建Parameter 对象。 35 /// </summary> 36 /// <returns>Parameter 对象。</returns> 37 public override IDataParameter CreateParameter() 38 { 39 return new SqlParameter(); 40 } 41 } 42 使用时,根据应用环境的数据库类型,创建相应的具体工厂对象即可。这样就避免了使用生硬的条件判断了。新增加一种类型的数据库时,只要添加一个类,从DbObjectFactory继承即可,而现有的代码不需要做任何的修改。(第8节描述了应用程序如何使用这些类) 5 主键的生成 不同的业务需要,生成主键的方式不同。(BTW,主键最好是无意义的)。一种简单的做法和第4节的[代码1]类似,缺点也很明显,不再赘述。 所有的主键生成器使用一个接口,不同类型的生成器实现改接口即可,然后由具体的应用程序来选择使用不同的主键生成器。(第8节描述了应用程序如何使用)
根据上面的类图,生成代码如下:
IdGenerator
1 using System; 2 /// <summary> 3 /// 关键码(Id)生成器接口。 4 /// </summary> 5 public interface IdGenerator 6 { 7 /// <summary> 8 /// 获取一个全局的关键码(ID)。 9 /// </summary> 10 /// <returns>关键码(ID)。</returns> 11 long GetNextId(); 12 13 /// <summary> 14 /// 获取指定KeyName上的一个关键码(ID)。 15 /// </summary> 16 /// <param name="keyName">关键码的名称/标识。</param> 17 /// <returns>关键码(ID)。</returns> 18 long GetNextId(string keyName); 19 } 20
CounterIdGenerator
1 /// <summary> 2 /// 关键码(Id)生成器。使用计数器。 3 /// </summary> 4 public class CounterIdGenerator : IdGenerator 5 { 6 /// <summary> 7 /// 键表,用于存储计数器关键码。 8 /// </summary> 9 protected Hashtable keyTable = new Hashtable(8); 10 11 /// <summary> 12 /// 构造函数。 13 /// </summary> 14 public CounterIdGenerator() 15 { 16 } 17 18 /// <summary> 19 /// 获取一个全局的关键码(ID)。 20 /// </summary> 21 /// <returns>关键码(ID)。</returns> 22 public long GetNextId() 23 { 24 return GetNextId("GlobalKey"); 25 } 26 27 /// <summary> 28 /// 获取指定KeyName上的一个关键码(ID)。 29 /// </summary> 30 /// <param name="keyName">关键码的名称/标识。</param> 31 /// <returns>关键码(ID)。</returns> 32 public long GetNextId(string keyName) 33 { 34 if(keyName == null || keyName == string.Empty) 35 { 36 throw new ArgumentNullException("keyName"); 37 } 38 39 long nextKey = 0; 40 41 if(keyTable.ContainsKey(keyName)) 42 { 43 nextKey = Convert.ToInt64(keyTable[keyName]); 44 } 45 else 46 { 47 keyTable.Add(keyName, nextKey); 48 } 49 50 keyTable[keyName] = ++nextKey; 51 52 return nextKey; 53 } 54 } 55
KeyTableIdGenerator
1 /// <summary> 2 /// 关键码(Id)生成器。使用键表。 3 /// </summary> 4 public class KeyTableIdGenerator : IdGenerator 5 { 6 /// <summary> 7 /// 构造函数。 8 /// </summary> 9 /// <param name="dbConfig">数据库配置。</param> 10 /// <param name="keyTableName">数据库中的键表名称。</param> 11 public KeyTableIdGenerator(DatabaseConfig dbConfig, string keyTableName) 12 { 13 _dbConfig = dbConfig; 14 _keyTableName = keyTableName; 15 } 16 17 /// <summary> 18 /// 数据库配置。 19 /// </summary> 20 private DatabaseConfig _dbConfig = null; 21 22 /// <summary> 23 /// 数据库中的键表名称。 24 /// </summary> 25 protected string _keyTableName = null; 26 27 /// <summary> 28 /// 读写数据库表KeyTable的模型。 29 /// </summary> 30 private KeyTableModel model = null; 31 32 /// <summary> 33 /// 读写数据库表KeyTable的模型。 34 /// </summary> 35 private KeyTableModel Model 36 { 37 get 38 { 39 if(model == null) 40 { 41 return model = new KeyTableModel(_dbConfig, _keyTableName); 42 } 43 else 44 { 45 return model; 46 } 47 } 48 } 49 50 /// <summary> 51 /// 获取一个关键码ID。 52 /// </summary> 53 /// <returns>关键码。</returns> 54 public long GetNextId() 55 { 56 return Model.GetNextKey(); 57 } 58 59 /// <summary> 60 /// 获取指定KeyName上的一个关键码(ID)。 61 /// </summary> 62 /// <param name="keyName">关键码的名称/标识。这里一般使用表名。</param> 63 /// <returns>关键码(ID)。</returns> 64 public long GetNextId(string keyName) 65 { 66 return Model.GetNextKey(keyName); 67 } 68 } 69 如果有新的ID生成规则,则从IdGenerator继承并应用即可。 6、数据库连接管理 方法和第5节的ID生成器一样。描述 从略。
7、SQL语句安全性检查 方法和第5节的ID生成器一样。描述从略。
8、组合起来(如何使用上面的东东) 上面谈到了如何适应不同的数据库,不同的主键生成,不同的数据库连接管理和SQL语句安全性检查,那么在具体某一中类型的应用程序中,如何使用他们呢? 不同的主键生成器,不同的数据库连接器,实现了不同的行为或算法,而不同应用程序程序就是不同的使用环境,因此采用策略模式是很自然的了。 策略模式设计到三个角色类: 环境角色:应用程序充当使用的环境 抽象策略角色:抽象类DatabaseConfig 具体策略:由应用程序实现的一个具体类,从DatabaseConfig类继承。 三个角色类如图7所示,Application是应用程序类,环境角色;DatabaseConfig是抽象策略类;XxxAppDbConfig是具体策略类,由该应用程序自己创建并实现。
DatabaseConfig
1 /// <summary> 2 /// 数据库配置。 3 /// (包括数据库连接管理器、数据库对象工厂管理器、关键字生成器和SQL语句检查器的配置)。 4 /// [Design Patterns]和类DbObjectFactory 一起,构成Strategy 模式。 5 /// </summary> 6 [Serializable] 7 public abstract class DatabaseConfig 8 { 9 #region 数据库对象工厂 10 11 /// <summary> 12 /// 数据库对象工厂。 13 /// </summary> 14 protected DbObjectFactory _dbObjectFactory = null; 15 16 /// <summary> 17 /// 创建数据库对象工厂。 18 /// </summary> 19 protected abstract DbObjectFactory NewDbObjectFactory(); 20 21 /// <summary> 22 /// 获取数据库对象工厂。 23 /// </summary> 24 public virtual DbObjectFactory DbObjectFactory 25 { 26 get 27 { 28 if(_dbObjectFactory == null) 29 { 30 _dbObjectFactory = NewDbObjectFactory(); 31 } 32 return _dbObjectFactory; 33 } 34 } 35 36 #endregion 37 38 #region 数据库链接管理器 39 40 /// <summary> 41 /// 数据库连接管理器。 42 /// </summary> 43 protected ConnectionManager _connectionManager = null; 44 45 /// <summary> 46 /// 创建数据库连接管理器。 47 /// </summary> 48 protected abstract ConnectionManager NewConnectionManager(); 49 50 /// <summary> 51 /// 获取数据库连接管理器。 52 /// </summary> 53 public virtual ConnectionManager ConnectionManager 54 { 55 get 56 { 57 if(_connectionManager == null) 58 { 59 _connectionManager = NewConnectionManager(); 60 } 61 return _connectionManager; 62 } 63 } 64 65 #endregion 66 67 #region 关键字生成器 68 69 /// <summary> 70 /// 关键字生成器。 71 /// </summary> 72 protected IdGenerator _idGenerator = null; 73 74 /// <summary> 75 /// 创建关键字生成器。 76 /// </summary> 77 /// <returns></returns> 78 protected abstract IdGenerator NewIdGenerator(); 79 80 /// <summary> 81 /// 获取关键字生成器。 82 /// </summary> 83 public virtual IdGenerator IdGenerator 84 { 85 get 86 { 87 if(_idGenerator == null) 88 { 89 _idGenerator = NewIdGenerator(); 90 } 91 return _idGenerator; 92 } 93 } 94 95 #endregion 96 97 #region SQL 语句检查器 98 99 /// <summary> 100 /// SQL 语句检查器。 101 /// </summary> 102 protected SqlStatementChecker _sqlStatementChecker; 103 104 /// <summary> 105 /// 创建SQL 语句检查器。 106 /// </summary> 107 protected abstract SqlStatementChecker NewSqlStatementChecker(); 108 109 /// <summary> 110 /// 获取SQL 语句检查器。 111 /// </summary> 112 public virtual SqlStatementChecker SqlStatementChecker 113 { 114 get 115 { 116 if(_sqlStatementChecker == null) 117 { 118 _sqlStatementChecker = NewSqlStatementChecker(); 119 } 120 return _sqlStatementChecker; 121 } 122 } 123 124 /// <summary> 125 /// 是否检查SQL 语句的安全性。 126 /// </summary> 127 protected bool _checkSqlStatement = false; 128 129 /// <summary> 130 /// 获取或设置是否检查SQL 语句的安全性。 131 /// </summary> 132 public virtual bool CheckSqlStatement 133 { 134 get 135 { 136 return _checkSqlStatement; 137 } 138 set 139 { 140 _checkSqlStatement = value; 141 } 142 } 143 144 #endregion 145 } 146
HfdDBConfig
1 /// <summary> 2 /// HFD系统的数据库配置。 3 /// </summary> 4 public class HfdDBConfig : DatabaseConfig 5 { 6 private static string _connectionString = null; 7 8 /// <summary> 9 /// 获取数据库连接字符串。 10 /// </summary> 11 public static string ConnectionString 12 { 13 get 14 { 15 if(_connectionString == null) 16 { 17 // 读取配置文件,获取数据库连接字符串。 18 // 19 _connectionString = "server=(local);database=HfdLog;uid=sa;pwd=1"; 20 } 21 return _connectionString; 22 } 23 } 24 25 protected override ConnectionManager NewConnectionManager() 26 { 27 // 每个进程和该数据库之间只有一个连接。 28 return new SingletonConnectionManager(ConnectionString, this); 29 } 30 31 protected override DbObjectFactory NewDbObjectFactory() 32 { 33 // 采用 SQL Server 数据库。 34 return new SqlObjectFactory(this); 35 } 36 37 protected override IdGenerator NewIdGenerator() 38 { 39 // 使用键表生成ID 40 return new KeyTableIdGenerator(this, "KeyTable"); 41 } 42 43 protected override SqlStatementChecker NewSqlStatementChecker() 44 { 45 // 不进行SQL语句的安全性检查。 46 return null; 47 } 48 49 } 50 9、表模型 至此,基本的结构已经搭建好了,但是距离数据库访问层还差一步,就是实现数据的访问模式,实现对物理数据库中的表、视图等的访问。 应用程序对数据库的访问有好几种方式: (1)事务脚本(存储过程)。一种面向过程的方法。 (2)ORM(对象-关系映射)。一种面向对象的方法。 (3)表模型。以物理数据表为基本单位进行访问,类似 .NET中的DataTable。 我觉得在 .NET中还是第三种方式更容易实现一点。 因为表和视图有很多相似点,不同的是视图是只读的,表是可读写的。因此建立一个基类DataModel(抽象类),提供表和视图相同的操作,然后为表和视图各建立一个类TableModel和ViewModel,从DataModel继承,并添加各自不同的操作。如图8:
DataModel中的GetView方法作为查询,TableModel中的Delete方法是删除,XxxModel中的Insert和Update方法分别是添加和修改。XxxModel是由实际的应用程序实现的,针对表xxx的数据模型。这样我们可以为每一个表和视图都建立一个Model,来实现对数据库表和视图的操作。这是数据访问层的核心。(注:DataModel中的TableName,KeyName是抽象属性,由基类(如XxxModel)实现)
DataModel
1 /// <summary> 2 3 /// 数据模型。 4 5 /// </summary> 6 7 [Serializable] 8 9 public abstract class DataModel 10 11 { 12 13 #region 构造函数 14 15 16 17 /// <summary> 18 19 /// 创建数据模型实例,并载入初始化数据到 Table。 20 21 /// </summary> 22 23 /// <param name="dbConfig">数据库配置。</param> 24 25 public DataModel(DatabaseConfig dbConfig) 26 27 { 28 29 if(dbConfig == null) 30 31 { 32 33 throw new ArgumentNullException("dbConfig"); 34 35 } 36 37 38 39 _dbConfig = dbConfig; 40 41 42 43 InitTable(); 44 45 } 46 47 48 49 /// <summary> 50 51 /// 数据库配置。 52 53 /// </summary> 54 55 protected DatabaseConfig _dbConfig = null; 56 57 58 59 #endregion 60 61 62 63 #region 属性 64 65 66 67 /// <summary> 68 69 /// 获取表名或视图名。 70 71 /// </summary> 72 73 public abstract string TableName 74 75 { 76 77 get; 78 79 } 80 81 82 83 /// <summary> 84 85 /// 获取关键字名。 86 87 /// </summary> 88 89 public abstract string KeyName 90 91 { 92 93 get; 94 95 } 96 97 98 99 /// <summary> 100 101 /// 该表的所有列的列名。 102 103 /// 用逗号隔开。用于 SELECT 语句中。 104 105 /// </summary> 106 107 /// <remarks> 108 109 /// 目的:避免使用SELECT * 。 110 111 /// </remarks> 112 113 public abstract string ColumnNames 114 115 { 116 117 get; 118 119 } 120 121 122 123 /// <summary> 124 125 /// 获取内蕴的 DataTable 对象。 126 127 /// </summary> 128 129 public DataTable Table 130 131 { 132 133 get 134 135 { 136 137 return _table; 138 139 } 140 141 } 142 143 /// <summary> 144 145 /// 内存表。 146 147 /// </summary> 148 149 protected DataTable _table = null; 150 151 152 153 /// <summary> 154 155 /// 获取 Table 中的记录的条数。 156 157 /// </summary> 158 159 public int RowCount 160 161 { 162 163 get 164 165 { 166 167 return _table.Rows.Count; 168 169 } 170 171 } 172 173 174 175 /// <summary> 176 177 /// 指示内存表 Table 中的字符串比较是否区分大小写。 178 179 /// </summary> 180 181 public bool CaseSensitive 182 183 { 184 185 get 186 187 { 188 189 return _table.CaseSensitive; 190 191 } 192 193 set 194 195 { 196 197 _table.CaseSensitive = value; 198 199 } 200 201 } 202 203 204 205 /// <summary> 206 207 /// 初始化内存表时使用的 Sql 语句。 208 209 /// </summary> 210 211 public abstract string InitalSelectCommandText 212 213 { 214 215 get; 216 217 } 218 219 220 221 #endregion 222 223 224 225 #region 获取数据库对象 226 227 228 229 /// <summary> 230 231 /// 获取数据库连接。 232 233 /// </summary> 234 235 protected DbConnection Connection 236 237 { 238 239 get 240 241 { 242 243 return (DbConnection)_dbConfig.ConnectionManager.GetConnection(); 244 245 } 246 247 } 248 249 250 251 /// <summary> 252 253 /// 获取一个DataAdapter对象。 254 255 /// </summary> 256 257 /// <returns>DataAdapter对象。</returns> 258 259 /// <remarks>返回的DataAdapter对象的SelectCommand的Connection属性已被设置。</remarks> 260 261 protected DbDataAdapter GetDataAdapter() 262 263 { 264 265 DbDataAdapter da = (DbDataAdapter)_dbConfig.DbObjectFactory.CreateDataAdapter(); 266 267 return da; 268 269 } 270 271 272 273 /// <summary> 274 275 /// 获取一个Command对象。 276 277 /// </summary> 278 279 /// <returns>Command对象。</returns> 280 281 /// <remarks>返回的Command对象的Connection属性已被设置。</remarks> 282 283 protected DbCommand GetCommand() 284 285 { 286 287 DbCommand cmd = (DbCommand)_dbConfig.DbObjectFactory.CreateCommand(); 288 289 cmd.Connection = Connection; 290 291 return cmd; 292 293 } 294 295 296 297 /// <summary> 298 299 /// 获取一个Parameter对象。 300 301 /// </summary> 302 303 /// <returns>Parameter对象。</returns> 304 305 protected DbParameter GetParameter() 306 307 { 308 309 return (DbParameter)_dbConfig.DbObjectFactory.CreateParameter(); 310 311 } 312 313 314 315 #endregion 316 317 318 319 #region 初始化内存表 320 321 322 323 /// <summary> 324 325 /// 从数据库中读取初始数据到内存表。 326 327 /// </summary> 328 329 /// <remarks> 330 331 /// 派生类最好能复写(override)该方法,但是派生类不需要要调用此方法,由基类的构造函数调用,类似于模板方法。 332 333 /// </remarks> 334 335 protected virtual void InitTable() 336 337 { 338 339 DbDataAdapter da = GetDataAdapter(); 340 341 342 343 try 344 345 { 346 347 _table = new DataTable(TableName); 348 349 350 351 da.SelectCommand = GetCommand(); 352 353 da.SelectCommand.CommandText = InitalSelectCommandText; 354 355 da.SelectCommand.CommandType = CommandType.Text; 356 357 358 359 // 载入数据。 360 361 da.Fill(_table); 362 363 364 365 // 设置数据表的主键。 366 367 _table.PrimaryKey = new DataColumn[] { _table.Columns[KeyName] }; 368 369 } 370 371 catch(Exception ex) 372 373 { 374 375 Debug.WriteLine( 376 377 ex.ToString() 378 379 + Environment.NewLine 380 381 + "SelectCommand为:" 382 383 + da.SelectCommand.CommandText 384 385 ); 386 387 } 388 389 } 390 391 392 393 /// <summary> 394 395 /// 强制重新从数据库中读取数据,刷新内存表。 396 397 /// </summary> 398 399 public void Refresh(string selectCommandText) 400 401 { 402 403 // 1、清除 DataTable 中原来的数据。 404 405 406 407 try 408 409 { 410 411 _table.Clear(); 412 413 _table.AcceptChanges(); 414 415 } 416 417 finally 418 419 { 420 421 } 422 423 424 425 426 427 // 2、载入新数据。 428 429 430 431 LoadData(selectCommandText); 432 433 } 434 435 436 437 /// <summary> 438 439 /// 从数据库加载数据。 440 441 /// </summary> 442 443 protected void LoadData(string selectCommandText) 444 445 { 446 447 DbDataAdapter da = GetDataAdapter(); 448 449 da.SelectCommand = GetCommand(); 450 451 da.SelectCommand.CommandText = selectCommandText; 452 453 da.SelectCommand.CommandType = CommandType.Text; 454 455 456 457 try 458 459 { 460 461 da.Fill(_table); 462 463 } 464 465 catch(Exception ex) 466 467 { 468 469 Debug.WriteLine( 470 471 ex.ToString() 472 473 + Environment.NewLine 474 475 + "SelectCommand为:" 476 477 + da.SelectCommand.CommandText 478 479 ); 480 481 } 482 483 finally 484 485 { 486 487 } 488 489 } 490 491 492 493 #endregion 494 495 496 497 #region SQL 命令(最好使用命名参数) 498 499 500 501 /// <summary> 502 503 /// 获取 SELECT 语句。 504 505 /// </summary> 506 507 /// <returns>SELECT 语句。没有 WHERE 子句 ,没有 ORDER BY 子句。</returns> 508 509 protected string GetSelectText() 510 511 { 512 513 return GetSelectText(null, null); 514 515 } 516 517 518 519 /// <summary> 520 521 /// 获取 SELECT 语句,并使用 rowFilter 作为 WHERE 子句,sort 作为 ORDER 子句。 522 523 /// </summary> 524 525 /// <param name="rowFilter"> 526 527 /// 条件语句。充当 WHERE 子句。 528 529 /// 最好使用命名参数,而不是拼接字符串。 530 531 /// 可以为空(null或string.Empty)。 532 533 /// </param> 534 535 /// <param name="sort">排序条件。作为 ORDER 子句。可以为空。</param> 536 537 /// <returns>SELECT 语句。</returns> 538 539 protected string GetSelectText(string rowFilter, string sort) 540 541 { 542 543 StringBuilder sb = new StringBuilder(); 544 545 sb.Append("SELECT "); 546 547 sb.Append(ColumnNames); 548 549 sb.Append(" FROM "); 550 551 sb.Append(TableName); 552 553 554 555 if(rowFilter != null && rowFilter != string.Empty) 556 557 { 558 559 sb.Append(" WHERE "); 560 561 sb.Append(rowFilter); 562 563 } 564 565 566 567 if(sort != null && sort != string.Empty) 568 569 { 570 571 sb.Append(" ORDER BY "); 572 573 sb.Append(sort); 574 575 } 576 577 578 579 return sb.ToString(); 580 581 } 582 583 584 585 #endregion 586 587 588 589 #region 查询(返回 DataView 对象) 590 591 592 593 /// <summary> 594 595 /// 获取 DataView 对象。 596 597 /// </summary> 598 599 /// <returns>DataView 对象。</returns> 600 601 /// <remarks> 602 603 /// DataView 是用于排序、筛选、搜索和导航的 DataTable 的自定义视图。 604 605 /// </remarks> 606 607 public DataView GetView() 608 609 { 610 611 return GetView(null); 612 613 } 614 615 616 617 /// <summary> 618 619 /// 根据指定的 rowFilter 获取 DataView 对象。 620 621 /// </summary> 622 623 /// <param name="rowFilter">要应用于 DataView 的条件语句。</param> 624 625 /// <returns>DataView 对象。如果返回 null,表示rowFilter 参数的语法错误。</returns> 626 627 /// <remarks> 628 629 /// rowFilter 参数的语法规则参见: 630 631 /// ms-help://MS.MSDNQTR.v80.chs/MS.MSDN.v80/MS.NETDEVFX.v20.chs/cpref4/html/P_System_Data_DataColumn_Expression.htm 632 633 /// </remarks> 634 635 public DataView GetView(string rowFilter) 636 637 { 638 639 return GetView(rowFilter, null, DataViewRowState.CurrentRows); 640 641 } 642 643 644 645 /// <summary> 646 647 /// 根据rowFilter中指定的条件,查询[TableName]表中的所有数据。 648 649 /// </summary> 650 651 /// <param name="rowFilter">条件语句。</param> 652 653 /// <param name="sort">排序条件。</param> 654 655 /// <returns>DataView 对象。如果返回 null,表示rowFilter 或 sort 参数的语法错误。</returns> 656 657 /// <remarks> 658 659 /// rowFilter 和 sort 参数的语法规则参见: 660 661 /// ms-help://MS.MSDNQTR.v80.chs/MS.MSDN.v80/MS.NETDEVFX.v20.chs/cpref4/html/P_System_Data_DataColumn_Expression.htm 662 663 /// </remarks> 664 665 public DataView GetView(string rowFilter, string sort) 666 667 { 668 669 return GetView(rowFilter, sort, DataViewRowState.CurrentRows); 670 671 } 672 673 674 675 /// <summary> 676 677 /// 根据指定的 rowFilter、sort 和 rowState 获取 DataView 对象。 678 679 /// </summary> 680 681 /// <param name="rowFilter">要应用于 DataView 的条件语句。</param> 682 683 /// <param name="sort">要应用于 DataView 的排序条件。</param> 684 685 /// <param name="rowState">要应用于 DataView 的 DataViewRowState。</param> 686 687 /// <returns>DataView 对象。如果返回 null,表示rowFilter 或 sort 参数的语法错误。</returns> 688 689 /// <remarks> 690 691 /// rowFilter 和 sort 参数的语法规则参见: 692 693 /// ms-help://MS.MSDNQTR.v80.chs/MS.MSDN.v80/MS.NETDEVFX.v20.chs/cpref4/html/P_System_Data_DataColumn_Expression.htm 694 695 /// </remarks> 696 697 public DataView GetView(string rowFilter, string sort, DataViewRowState rowState) 698 699 { 700 701 DataView view = null; 702 703 try 704 705 { 706 707 view = new DataView(_table); 708 709 710 711 if(rowFilter != null && rowFilter != string.Empty) 712 713 { 714 715 view.RowFilter = rowFilter; 716 717 } 718 719 720 721 if(sort != null && sort != string.Empty) 722 723 { 724 725 view.Sort = sort; 726 727 } 728 729 730 731 view.RowStateFilter = rowState; 732 733 } 734 735 catch(Exception ex) 736 737 { 738 739 Debug.WriteLine(ex.ToString()); 740 741 return null; 742 743 } 744 745 746 747 return view; 748 749 } 750 751 752 753 #endregion 754 755 756 757 #region 获取数据 758 759 760 761 /// <summary> 762 763 /// 获取关键字为 id 的 DataRow。 764 765 /// </summary> 766 767 /// <param name="id">关键字。</param> 768 769 /// <returns>如果存在,返回 DataRow 对象。否则,返回 null。</returns> 770 771 public DataRow GetRow(long id) 772 773 { 774 775 DataRow[] rows = _table.Select(KeyName + "=" + id.ToString()); 776 777 778 779 if(rows.Length == 0) 780 781 { 782 783 return null; 784 785 } 786 787 else if(rows.Length == 1) 788 789 { 790 791 return rows[0]; 792 793 } 794 795 else 796 797 { 798 799 StringBuilder sb = new StringBuilder(); 800 801 sb.Append("严重错误:关键字重复。"); 802 803 sb.Append(Environment.NewLine); 804 805 sb.Append(" 表["); 806 807 sb.Append(TableName); 808 809 sb.Append("]中的关键字["); 810 811 sb.Append(id); 812 813 sb.Append("]重复"); 814 815 sb.Append(rows.Length); 816 817 sb.Append("次。"); 818 819 throw new Exception(sb.ToString()); 820 821 } 822 823 } 824 825 826 827 #endregion 828 829 830 831 } 832
TableModel
1 /// <summary> 2 /// 表模型。用于操作数据库表。 3 /// </summary> 4 [Serializable] 5 public abstract class TableModel : DataModel 6 { 7 #region 构造函数 8 9 /// <summary> 10 /// 创建数据模型实例,并载入初始化数据到 Table。 11 /// </summary> 12 /// <param name="dbConfig">数据库配置。</param> 13 public TableModel(DatabaseConfig dbConfig) 14 : base(dbConfig) 15 { 16 } 17 18 #endregion 19 20 #region 删除 21 22 /// <summary> 23 /// 删除指定id的记录。 24 /// 该方法不会影响内存表Table。 25 /// </summary> 26 /// <param name="id">关键码。</param> 27 /// <returns> 28 /// 包含三个元素的string数组。 29 /// [0]:"fail"或"success",分别指示成功或失败。 30 /// [1]:失败的原因(异常) 或 成功操作影响的行数,正常情况下,该值应该为0或1。 31 /// [2]:执行的SQL语句。 32 /// </returns> 33 public virtual string[] Delete(long id) 34 { 35 // 1、删除数据库中的数据。 36 37 IDbCommand cmd = GetCommand(); 38 cmd.CommandText = GetDeleteText(); 39 cmd.CommandType = CommandType.Text; 40 41 DbParameter idParam = GetParameter(); 42 idParam.ParameterName = "@" + KeyName; 43 idParam.Value = id; 44 cmd.Parameters.Add(idParam); 45 46 int affectedRows = -1; 47 try 48 { 49 affectedRows = cmd.ExecuteNonQuery(); 50 } 51 catch(Exception ex) 52 { 53 return new string[] { "fail", ex.ToString(), cmd.CommandText }; 54 } 55 56 57 // 2、删除内存表DataTable中的数据。 58 59 if(_table != null) 60 { 61 try 62 { 63 DataRow[] rows = _table.Select(KeyName + "=" + id); 64 foreach(DataRow row in rows) 65 { 66 row.Delete(); 67 } 68 _table.AcceptChanges(); 69 } 70 catch(Exception ex) // 数据库已经删除成功,但是内存表更新失败。 71 { 72 return new string[] { "warning", ex.ToString(), cmd.CommandText }; 73 } 74 finally 75 { 76 } 77 } 78 79 return new string[] { "success", Convert.ToString(affectedRows), cmd.CommandText }; 80 } 81 82 #endregion 83 84 #region 关键码 85 86 protected long GetNextId() 87 { 88 return _dbConfig.IdGenerator.GetNextId(TableName); 89 } 90 91 #endregion 92 93 #region 检查 94 95 /// <summary> 96 /// 字符串检查。 97 /// 检查规则: 98 /// 根据canBeEmpty指示的值检查是否为空值。(空值:null或String.Empty) 99 /// 如果不为空,长度应小于 maxLength 指示的值。 100 /// </summary> 101 /// <param name="str">待检查的字符串。</param> 102 /// <param name="canBeEmpty">是否可以为空(null或String.Empty)。</param> 103 /// <param name="maxLength">字符串的最大长度。</param> 104 /// <returns>如果此方法成功,则为 true;否则为 false。</returns> 105 /// <remarks>String.Empty 等价于 "",其长度为 0。</remarks> 106 public virtual bool CheckString(string str, bool canBeEmpty, int maxLength) 107 { 108 if(str == null || str == string.Empty) 109 { 110 if(canBeEmpty) 111 { 112 return true; 113 } 114 else 115 { 116 return false; 117 } 118 } 119 else 120 { 121 if(str.Length <= maxLength) 122 { 123 return true; 124 } 125 126 return false; 127 } 128 } 129 130 #endregion 131 132 #region SQL 命令(删除) 133 134 /// <summary> 135 /// 获取 DELETE 命令,用以删除关键码等于命名参数@KeyName的值的记录。 136 /// </summary> 137 /// <returns>带命名参数的 DELETE 命令。</returns> 138 protected string GetDeleteText() 139 { 140 if(_deleteText != null) 141 { 142 return _deleteText; 143 } 144 145 StringBuilder sb = new StringBuilder(); 146 sb.Append("DELETE FROM "); 147 sb.Append(TableName); 148 sb.Append(" WHERE "); 149 sb.Append(KeyName); 150 sb.Append("=@"); 151 sb.Append(KeyName); 152 153 return _deleteText = sb.ToString(); 154 } 155 156 /// <summary> 157 /// 带命名参数的 DELETE 命令。 158 /// </summary> 159 private string _deleteText = null; 160 161 #endregion 162 163 } 164
StationModel
1 /// <summary> 2 /// [车站]的数据模型。 3 /// 用于操作数据表[Station]。 4 /// </summary> 5 [Serializable] 6 public class StationModel : TableModel 7 { 8 #region 常量 9 10 // 11 // 字段长度的定义。 12 // 和数据库中相应字段的长度一致。 13 // 用于参数校验。 14 // 15 16 public const int CzmcMaxLength = 50; 17 18 #endregion 19 20 #region 构造函数 21 22 /// <summary> 23 /// 构造函数。 24 /// </summary> 25 /// <param name="dbConfig">数据库配置。</param> 26 public StationModel(DatabaseConfig dbConfig) 27 : base(dbConfig) 28 { 29 } 30 31 #endregion 32 33 #region 属性 34 35 /// <summary> 36 /// 表名。 37 /// </summary> 38 public override string TableName 39 { 40 get 41 { 42 return "Station"; 43 } 44 } 45 46 /// <summary> 47 /// 关键码名。 48 /// </summary> 49 public override string KeyName 50 { 51 get 52 { 53 return "stationID"; 54 } 55 } 56 57 /// <summary> 58 /// 该表的所有列的列名。 59 /// 用逗号隔开。用于 SELECT 语句中。 60 /// </summary> 61 /// <remarks> 62 /// 目的:避免使用SELECT * 。 63 /// </remarks> 64 public override string ColumnNames 65 { 66 get 67 { 68 return "stationID,czmc"; 69 } 70 } 71 72 public override string InitalSelectCommandText 73 { 74 get 75 { 76 return GetSelectText(); 77 } 78 } 79 #endregion 80 81 #region 添加INSERT 82 83 /// <summary> 84 /// 添加一条记录。 85 /// </summary> 86 /// <param name="czmc">数据项名称</param> 87 /// <returns> 88 /// 包含三个元素的string数组。 89 /// [0]:"warning" | "fail" | "success",分别指示警告,失败,成功。 90 /// [1]:失败时,指示失败的原因(即异常信息) ; 成功时,指示新记录的关键码。 91 /// [2]:执行的SQL语句。 92 /// </returns> 93 public string[] Insert( 94 string czmc 95 ) 96 { 97 #region 1、参数检查 98 99 if(!Check(czmc)) 100 { 101 return new string[] { "fail", "参数不合法,参数检查没通过。", "" }; 102 } 103 104 #endregion 105 106 long id = GetNextId(); // 获取关键码。 107 108 #region 2、生成 INSERT 语句 109 110 StringBuilder sb = new StringBuilder(); 111 112 sb.Append("INSERT INTO "); 113 sb.Append(TableName); 114 sb.Append("("); 115 sb.Append(KeyName); 116 sb.Append(",czmc) "); 117 sb.Append("VALUES(@"); 118 sb.Append(KeyName); 119 sb.Append(",@czmc)"); 120 121 #endregion 122 123 #region 3、执行 124 125 DbCommand cmd = GetCommand(); 126 cmd.CommandText = sb.ToString(); 127 cmd.CommandType = CommandType.Text; 128 129 SetParameters(cmd, id, czmc); 130 131 int affectedRows = -1; 132 try 133 { 134 affectedRows = cmd.ExecuteNonQuery(); 135 } 136 catch(Exception ex) 137 { 138 return new string[] { "fail", ex.ToString(), cmd.CommandText }; 139 } 140 141 #endregion 142 143 #region 4、更新内存表 144 if(_table != null) 145 { 146 try 147 { 148 DataRow newRow = _table.NewRow(); 149 newRow[KeyName] = id; 150 151 SetRow(newRow, czmc); 152 153 _table.Rows.Add(newRow); 154 _table.AcceptChanges(); 155 } 156 catch(Exception ex) // 数据库已经修改成功,但是内存表更新失败。 157 { 158 return new string[] { "warning", ex.ToString(), cmd.CommandText }; 159 } 160 finally 161 { 162 } 163 } 164 #endregion 165 166 return new string[] { "success", Convert.ToString(id), cmd.CommandText }; 167 } 168 169 170 #endregion 171 172 #region 修改UPDATE 173 174 /// <summary> 175 /// 添加一条记录。 176 /// </summary> 177 /// <param name="id">待修改记录的主键码。</param> 178 /// <param name="czmc">数据项名称</param> 179 /// <returns> 180 /// 包含三个元素的string数组。 181 /// [0]:"warning" | "fail" | "success",分别指示警告,失败,成功。 182 /// [1]:失败时,指示失败的原因(即异常信息) ; 成功时,指示被修改记录的关键码。 183 /// [2]:执行的SQL语句。 184 /// </returns> 185 public string[] Update( 186 long id, 187 string czmc 188 ) 189 { 190 #region 1、参数检查 191 192 if(!Check(czmc)) 193 { 194 return new string[] { "fail", "参数不合法,参数检查没通过。", "" }; 195 } 196 197 #endregion 198 199 #region 2、生成UPDATE语句 200 201 StringBuilder sb = new StringBuilder(); 202 203 sb.Append("UPDATE "); 204 sb.Append(TableName); 205 sb.Append(" SET czmc=@czmc "); 206 sb.Append(" WHERE "); 207 sb.Append(KeyName); 208 sb.Append("=@" + KeyName); 209 210 #endregion 211 212 #region 3、执行 213 214 DbCommand cmd = GetCommand(); 215 cmd.CommandText = sb.ToString(); 216 cmd.CommandType = CommandType.Text; 217 218 SetParameters(cmd, id, czmc); 219 220 int affectedRows = -1; 221 try 222 { 223 affectedRows = cmd.ExecuteNonQuery(); 224 } 225 catch(Exception ex) 226 { 227 return new string[] { "fail", ex.ToString(), cmd.CommandText }; 228 } 229 230 #endregion 231 232 #region 4、更新内存表 233 234 if(_table != null) 235 { 236 try 237 { 238 DataRow[] rows = _table.Select(KeyName + "=" + Convert.ToString(id)); 239 240 if(rows.Length == 1) 241 { 242 DataRow row = rows[0]; 243 244 SetRow(row, czmc); 245 246 _table.AcceptChanges(); 247 } 248 else 249 { 250 throw new Exception("表[" + TableName + "]中,关键码[" + Convert.ToString(id) + "]重复或为0,数量:" + Convert.ToString(rows.Length) + "。"); 251 } 252 } 253 catch(Exception ex) // 数据库已经修改成功,但是内存表更新失败。 254 { 255 return new string[] { "warning", ex.ToString(), cmd.CommandText }; 256 } 257 finally 258 { 259 } 260 } 261 #endregion 262 263 return new string[] { "success", Convert.ToString(id), cmd.CommandText }; 264 } 265 266 #endregion 267 268 #region 其他 269 /// <summary> 270 /// 为 Command 对象添加相应的参数并设定参数的值。 271 /// 一般用于 UPDATA 和 INSERT 两个操作。 272 /// 注意:参数的名称必须相同。 273 /// </summary> 274 /// <param name="cmd">待设定的 Command 对象。</param> 275 /// <param name="id">记录的主键码。</param> 276 /// <param name="czmc">数据项名称</param> 277 /// <param name="description">描述说明</param> 278 /// <param name="address">PLC 地址</param> 279 /// <param name="dataType">PLC 中对应的数据类型。</param> 280 private void SetParameters(DbCommand cmd, 281 long id, 282 string czmc) 283 { 284 DbParameter idParam = GetParameter(); 285 idParam.ParameterName = "@" + KeyName; 286 idParam.Value = id; 287 cmd.Parameters.Add(idParam); 288 289 DbParameter czmcParam = GetParameter(); 290 czmcParam.ParameterName = "@czmc"; 291 czmcParam.Value = czmc; 292 cmd.Parameters.Add(czmcParam); 293 } 294 295 /// <summary> 296 /// 设置 DataRow 的数据。 297 /// </summary> 298 /// <param name="row">待设定的 DataRow 对象。</param> 299 /// <param name="czmc">数据项名称</param> 300 /// <param name="description">描述说明</param> 301 /// <param name="address">PLC 地址</param> 302 /// <param name="dataType">PLC 中对应的数据类型。</param> 303 private static void SetRow(DataRow row, string czmc) 304 { 305 row["czmc"] = czmc; 306 } 307 #endregion 308 309 #region 参数检查 310 311 // 312 // 1、检查是否符合数据库中字段类型长度等。 313 // 2、根据业务规则,检查参数的值是否合法。可以使用正则表达式等工具进行检查。 314 // 315 316 /// <summary> 317 /// 检查车站名称czmc是否可以成功保存到数据库。 318 /// </summary> 319 /// <param name="czmc">车站名称。</param> 320 /// <returns>如果此方法成功,则为 true;否则为 false。 </returns> 321 public bool CheckCzmc(string czmc) 322 { 323 return CheckString(czmc, false, CzmcMaxLength); 324 } 325 326 /// <summary> 327 /// 检查参数组合是否合法。 328 /// </summary> 329 /// <param name="czmc">数据项名称</param> 330 /// <param name="description">描述说明</param> 331 /// <param name="address">PLC 地址</param> 332 /// <param name="dataType">PLC 中对应的数据类型。</param> 333 /// <returns>如果此方法成功,则为 true;否则为 false。</returns> 334 public bool Check( string czmc ) 335 { 336 if(CheckCzmc(czmc)) 337 { 338 return true; 339 } 340 341 return false; 342 } 343 344 #endregion 345 } 346 从上面的代码可以看出,所有的 SQL语句都到了Model类中,业务逻辑层和应用程序层都不需要书写SQL语句,减轻了数据库变化带来的影响。10、结束语 吁~,终于写完了。以上是我的设计,很多都没有考虑到,比如性能啊等等,因为水平有限,其中有很多问题和错误,请大家的评点和指教,谢谢! |
|
来自: kittywei > 《sqlserver》