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] 简单,但缺乏弹性和复用性的方法
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所示的规律:
图2 不同数据库和对象的关系
很明显,采用抽象工厂模式可以很好的处理不同数据库和不同数据库对象之间的组和系列的关系。如图3:
图3 数据库对象工厂设计
根据上面的类图,生成C#代码如下:
[代码2: DbObjectFactory.cs] (有删节)
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
[代码3: OleDbObjectFactory.cs] (有删节)
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
[代码4: OleDbObjectFactory.cs] (有删节)
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节描述了应用程序如何使用)
图4 关键字生成器类图
根据上面的类图,生成代码如下:
[代码5: IdGenerator.cs]
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
[代码6: CounterIdGenerator.cs]
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
[代码7:KeyTableIdGenerator.cs]
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生成器一样。描述 从略。
图5 数据库连接管理类图
7、SQL语句安全性检查
方法和第5节的ID生成器一样。描述从略。
图6 SQL语句检查器类图
8、组合起来(如何使用上面的东东)
上面谈到了如何适应不同的数据库,不同的主键生成,不同的数据库连接管理和SQL语句安全性检查,那么在具体某一中类型的应用程序中,如何使用他们呢?
不同的主键生成器,不同的数据库连接器,实现了不同的行为或算法,而不同应用程序程序就是不同的使用环境,因此采用策略模式是很自然的了。
策略模式设计到三个角色类:
环境角色:应用程序充当使用的环境
抽象策略角色:抽象类DatabaseConfig
具体策略:由应用程序实现的一个具体类,从DatabaseConfig类继承。
三个角色类如图7所示,Application是应用程序类,环境角色;DatabaseConfig是抽象策略类;XxxAppDbConfig是具体策略类,由该应用程序自己创建并实现。
图7 策略模式的使用
[代码8:DatabaseConfig.cs]
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
[代码9:XxxAppDbConfig.cs]
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:
图8 表模型类图
DataModel中的GetView方法作为查询,TableModel中的Delete方法是删除,XxxModel中的Insert和Update方法分别是添加和修改。XxxModel是由实际的应用程序实现的,针对表xxx的数据模型。这样我们可以为每一个表和视图都建立一个Model,来实现对数据库表和视图的操作。这是数据访问层的核心。(注:DataModel中的TableName,KeyName是抽象属性,由基类(如XxxModel)实现)
[代码10:DataModel.cs]
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
[代码11:TableModel.cs]
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
[代码12:XxxModel.cs]
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、结束语
吁~,终于写完了。以上是我的设计,很多都没有考虑到,比如性能啊等等,因为水平有限,其中有很多问题和错误,请大家的评点和指教,谢谢!