如何简化数据库的访问与操作,兼谈泛型的应用
一、一些说明及申明
不积跬步无以至千里,不纳滴水无以成江海。本文谈到的仅仅是实际开发过程的一个简单应用,虽然显得很不深奥,但并不影响一些编程思想在其中的具体体现。授之与鱼,不如授之与渔。我期望的是,通过某个简单应用的编码过程,让大家理解某种(个)技术的发明初衷,以及它的适用场景。
可惜的是,这里大量的内容基于个人的经验总结和理解,毕竟书读得不够、不透,如果某些措辞不当或者指代混乱希望您见谅,好在不是正式出版物,草根说错话肯定比“专家”要少挨很多砖:)
不过两年多的工作经历,自我感觉技术上尚是浅薄,师弟、师妹们可多了解些我的工作心得,或许能少走弯路;至于大侠、小虾之辈,哂笑之后若肯指教一二最好,或者请以宽容之心释己之怀啊!
二、面向对象的抽象过程
在进行后台开发时,我们时常要进行数据库的访问与操作,.net 提供了 IDbConnection, IDbCommand 以及 IDbDataParameter 等接口可以满足我们的日常需要。通常,我们这样获取数据或进行操作:
1: //ExecuteReader
2: using (SqlConnection con = new SqlConnection("my connection string")) {
3: using (SqlCommand cmd = new SqlCommand("my command string", con)) {
4: con.Open();
5: using (SqlDataReader reader = cmd.ExecuteReader()) {
6: while (reader.Read()) {
7: //Todo
8: }
9: }
10: }
11: }
12:
13: //ExecuteScalar
14: using (SqlConnection con = new SqlConnection("my connection string")) {
15: using (SqlCommand cmd = new SqlCommand("my command string", con)) {
16: con.Open();
17: object result = cmd.ExecuteScalar();
18: }
19: }
20:
21: //ExecuteNonQuery
22: using (SqlConnection con = new SqlConnection("my connection string")) {
23: using (SqlCommand cmd = new SqlCommand("my command string", con)) {
24: con.Open();
25: int result = cmd.ExecuteNonQuery();
26: }
27: }
我在做了相当长一段时间如上编码之后,发觉可以进行如下简化,创建 SqlHelper 进行辅助。相信当前很多开发人员也是这么做的(指思路相似,细节处肯定不同):
1: class SqlHelper {
2: public static SqlDataReader ExecuteReader(string conStr, string cmdStr) {
4: //ExecuteReader
5: SqlConnection con = new SqlConnection(conStr);
6: SqlCommand cmd = new SqlCommand(cmdStr, con);
7: con.Open();
8: return cmd.ExecuteReader(System.Data.CommandBehavior.CloseConnection);//这表示操作完成后关闭该 Reader
9: }
10:
11: public static object ExecuteScalar(string conStr, string cmdStr) {
12: //ExecuteScalar
13: using (SqlConnection con = new SqlConnection(conStr)) {
14: using (SqlCommand cmd = new SqlCommand(cmdStr, con)) {
15: con.Open();
16: return cmd.ExecuteScalar();
17: }
18: }
19: }
20:
21: public static int ExecuteNonQuery(string conStr, string cmdStr) {
22: //ExecuteNonQuery
23: using (SqlConnection con = new SqlConnection(conStr)) {
24: using (SqlCommand cmd = new SqlCommand(cmdStr, con)) {
25: con.Open();
26: return cmd.ExecuteNonQuery();
27: }
28: }
29: }
30: }
当需要传参时,要么通过 string.format() 等方法拼接 cmdStr (很不安全),要么通过原始方法,或者通过如下变通方案:
1: struct Parameter{
2: public string Name{get;set;}
3: public object Value{get;set;}
4: }
5: public static int ExecuteNonQuery(string conStr, string cmdStr, params Parameter[] pars) {
6: //ExecuteNonQuery
7: using (SqlConnection con = new SqlConnection(conStr)) {
8: using (SqlCommand cmd = new SqlCommand(cmdStr, con)) {
9: foreach (Parameter p in pars) {
10: cmd.Parameters.AddWithValue(p.Name, p.Value);
11: }
12: con.Open();
13: return cmd.ExecuteNonQuery();
14: }
15: }
16: }
到这一步,看上去数据库访问已经简化得差不多:
int result = SqlHelper.ExecuteNonQuery("my connection string", "my command string", new Parameter() { Name = "Number", Value = 1 }, new Parameter() { Name = "Text", Value = "afaefawf" });
或者只是一些细节上的优化:
1: class SqlHelperII {
2:
3: public static SqlHelperII InstanceForStudentsSystem { get; private set; }
4:
5: static SqlHelperII() {
6: InstanceForStudentsSystem = new SqlHelperII(System.Configuration.ConfigurationSettings
.AppSettings["StudentsSystemConnectionString"]);
7: }
8:
9: public SqlHelperII(string connectionString) {
10: this.ConnectionString = connectionString;
11: }
12:
13: public string ConnectionString { get; private set; }
14:
15: public int ExecuteNonQuery(string cmdStr, params string[] pars) {
16: //ExecuteNonQuery
17: using (SqlConnection con = new SqlConnection(this.ConnectionString)) {
18: using (SqlCommand cmd = new SqlCommand(cmdStr, con)) {
19: for (int i = 0; i < pars.Length; i += 2) {
20: cmd.Parameters.AddWithValue(pars[i], pars[i + 1]);
21: }
22: con.Open();
23: return cmd.ExecuteNonQuery();
24: }
25: }
26: }
27:
28: }
以往的操作可以这样进行:
1: int result = SqlHelperII.InstanceForStudentsSystem.ExecuteNonQuery("my command string",
"Number", 1.ToString(), "Text", "afaefawf");
事实上,一般情况下我个人认为这样确实足够了,但上面的代码全假设在了 SqlServer 数据库之上,一旦遇到 MySql 之类,就需要编写某个 MySqlHelper 等。另外,我在年初某个项目与 MID 层进行交互时突然悟到,其实任何数据类操作都可以抽象成类数据库的访问方式;当时我为此做了个 MIDServicesHelper,其实现了 ExecuteReader,ExecuteScalar,ExecuteNonQuery 三个方法(我的实践证明,这三个方法大概是足够用了)。
接下来的时间里,我创建了这样的一个接口 IDbExecutor:
1: namespace Queen.Data {
2:
3: /// <summary>
4: /// 数据库服务接口, 负责进行数据的存储、读取等基本操作.
5: /// 它可以是一个传统的 Sql 数据库, 也可以是其他支持数据操作的系统.
6: /// </summary>
7: public interface IDbExecutor {
8:
9: /// <summary>
10: /// 链接串
11: /// </summary>
12: string ConnectionString {
13: get;
14: set;
15: }
16:
17: /// <summary>
18: /// 超时设置
19: /// </summary>
20: int CommandTimeout {
21: get;
22: set;
23: }
24:
25: /// <summary>
26: /// 获取执行影响行数.
27: /// 实现该接口时注意释放所有相关资源!
28: /// </summary>
29: /// <param name="cmdText"></param>
30: /// <param name="keyValuePairs"></param>
31: /// <returns></returns>
32: int ExecuteNonQuery(string cmdText, params string[] keyValuePairs);
33:
34: /// <summary>
35: /// 获取执行值.
36: /// 实现该接口时注意释放所有相关资源!
37: /// </summary>
38: /// <param name="cmdText"></param>
39: /// <param name="keyValuePairs"></param>
40: /// <returns></returns>
41: object ExecuteScalar(string cmdText, params string[] keyValuePairs);
42:
43: /// <summary>
44: /// 获取数据读取流.
45: /// 调用该接口时务必释放读取流, using(IDataReader reader = XX.ExecuteReader()){ ... }
46: /// 实现该接口时请勿释放任何资源, 返回调用 System.Data.SqlCommand.ExecuteReader(
System.Data.CommandBehavior.CloseConnection)
47: /// </summary>
48: /// <param name="cmdText">数据库执行文本.</param>
49: /// <param name="keyValuePairs">参数组, 比如 "@ParaA", "valueA", "@ParaB", "valueB"</param>
50: /// <returns>数据读出流</returns>
51: System.Data.IDataReader ExecuteReader(string cmdText, params string[] keyValuePairs);
52:
53: }
54: }
凡后来我主导的项目里总会多出这样一个文件及类型 DbExecutor,其内部可定义支持 SqlServer、MySql 甚至其它数据库的内部类,但数据库类型对外界“基本”透明(组织 SQL 语句时必须知道数据库类型,SqlServer、MySql 有各自的 SQL 语言特性!):
1: static class DbExecutor {
2:
3: public static Queen.Data.IDbExecutor Students { get; private set; }
4: public static Queen.Data.IDbExecutor Families { get; private set; }
5:
6: static DbExecutor() {
7: //Init students, families ...
8: }
9:
10: private class SqlServerExecutor : Queen.Data.IDbExecutor {
11:
12: #region IDbExecutor 成员
13:
14: public string ConnectionString {
15: get {
16: throw new NotImplementedException();
17: }
18: set {
19: throw new NotImplementedException();
20: }
21: }
22:
23: public int CommandTimeout {
24: get {
25: throw new NotImplementedException();
26: }
27: set {
28: throw new NotImplementedException();
29: }
30: }
31:
32: public int ExecuteNonQuery(string cmdText, params string[] keyValuePairs) {
33: throw new NotImplementedException();
34: }
35:
36: public object ExecuteScalar(string cmdText, params string[] keyValuePairs) {
37: throw new NotImplementedException();
38: }
39:
40: public System.Data.IDataReader ExecuteReader(string cmdText, params string[] keyValuePairs) {
41: throw new NotImplementedException();
42: }
43:
44: #endregion
45: }
9:
10: private class MySqlExecutor : Queen.Data.IDbExecutor {
11:
12: #region IDbExecutor 成员
13:
14: public string ConnectionString {
15: get {
16: throw new NotImplementedException();
17: }
18: set {
19: throw new NotImplementedException();
20: }
21: }
22:
23: public int CommandTimeout {
24: get {
25: throw new NotImplementedException();
26: }
27: set {
28: throw new NotImplementedException();
29: }
30: }
31:
32: public int ExecuteNonQuery(string cmdText, params string[] keyValuePairs) {
33: throw new NotImplementedException();
34: }
35:
36: public object ExecuteScalar(string cmdText, params string[] keyValuePairs) {
37: throw new NotImplementedException();
38: }
39:
40: public System.Data.IDataReader ExecuteReader(string cmdText, params string[] keyValuePairs) {
41: throw new NotImplementedException();
42: }
43:
44: #endregion
45: }
46: }
这样做的好处在于,不但在代码量上减少了开发人员的维护成本,也在系统的迁移上极大减少了代码替换工作(虽然迁移的可能性比较小),同时,也给系统的数据库操作监控带来了便利(比如增加日志功能等,后文将举例实现)。
三、泛型编程
但针对数据库操作这块,我还是严重感觉不够优雅,我觉得 SqlServerExecutor、MyServerExecutor 等虽然引用了不同的驱动程序,但这些驱动程序均实现了同一类接口(IDbConnection等),通过继承实现多个版本的 XxxExecutor 简直太丑陋了!而后,通过学习、领会泛型编程思想,我当前使用的版本产生了:
1: using System.Data;
2:
3: namespace Queen.Data {
4: /// <summary>
5: /// 提供 Sql 语言支持的数据库服务, 必须声明为 IDbExecutor 才能使用相关服务.
6: /// </summary>
7: /// <typeparam name="TConnection">链接类型</typeparam>
8: /// <typeparam name="TCommand">命令类型</typeparam>
9: /// <typeparam name="TDataParameter">数据参数类型</typeparam>
10: public class SqlExecutor<TConnection, TCommand, TDataParameter> : IDbExecutor
11: where TConnection : IDbConnection, new()
12: where TCommand : IDbCommand, new()
13: where TDataParameter : IDbDataParameter, new() {
14:
15: /// <summary>
16: /// Sql 命令类型.
17: /// </summary>
18: public System.Data.CommandType CommandType {
19: get;
20: set;
21: }
22:
23: #region Construct
24:
25: public SqlExecutor() {
26: CommandType = System.Data.CommandType.Text;
27: ((IDbExecutor)this).CommandTimeout = 30;
28: }
29:
30: public SqlExecutor(System.Data.CommandType commandType) {
31: CommandType = commandType;
32: ((IDbExecutor)this).CommandTimeout = 30;
33: }
34:
35: public SqlExecutor(System.Data.CommandType commandType, int commandTimeout) {
36: CommandType = commandType;
37: ((IDbExecutor)this).CommandTimeout = commandTimeout;
38: }
39:
40: #endregion
41:
42: #region ISqlExecutor 成员
43:
44: string IDbExecutor.ConnectionString {
45: get;
46: set;
47: }
48:
49: /// <summary>
50: /// 链接串, 默认 30(秒)
51: /// </summary>
52: int IDbExecutor.CommandTimeout {
53: get;
54: set;
55: }
56:
57: int IDbExecutor.ExecuteNonQuery(string cmdText, params string[] keyValuePairs) {
58: using (TConnection con = new TConnection()) {
59: con.ConnectionString = ((IDbExecutor)this).ConnectionString;
60: using (TCommand cmd = new TCommand()) {
61: cmd.CommandText = cmdText;
62: cmd.Connection = con;
63: if (keyValuePairs != null) {
64: for (int i = 0; i < keyValuePairs.Length - 1; i += 2) {
65: TDataParameter p = new TDataParameter();
66: p.ParameterName = keyValuePairs[i];
67: p.Value = keyValuePairs[i + 1];
68: cmd.Parameters.Add(p);
69: }
70: }
71: cmd.CommandType = CommandType;
72: cmd.CommandTimeout = ((IDbExecutor)this).CommandTimeout;
73: con.Open();
74: return cmd.ExecuteNonQuery();
75: }
76: }
77: }
78:
79: object IDbExecutor.ExecuteScalar(string cmdText, params string[] keyValuePairs) {
80: using (TConnection con = new TConnection()) {
81: con.ConnectionString = ((IDbExecutor)this).ConnectionString;
82: using (TCommand cmd = new TCommand()) {
83: cmd.CommandText = cmdText;
84: cmd.Connection = con;
85: if (keyValuePairs != null) {
86: for (int i = 0; i < keyValuePairs.Length - 1; i += 2) {
87: TDataParameter p = new TDataParameter();
88: p.ParameterName = keyValuePairs[i];
89: p.Value = keyValuePairs[i + 1];
90: cmd.Parameters.Add(p);
91: }
92: }
93: cmd.CommandType = CommandType;
94: cmd.CommandTimeout = ((IDbExecutor)this).CommandTimeout;
95: con.Open();
96: return cmd.ExecuteScalar();
97: }
98: }
99: }
100:
101: /// <summary>
102: /// 获取数据读取流
103: /// 调用该接口时务必释放读取流, using(IDataReader reader = XX.ExecuteReader()){ ... }.
104: /// </summary>
105: /// <param name="cmdText"></param>
106: /// <param name="keyValuePairs"></param>
107: /// <returns></returns>
108: System.Data.IDataReader IDbExecutor.ExecuteReader(string cmdText, params string[] keyValuePairs) {
109: TConnection con = new TConnection();
110: con.ConnectionString = ((IDbExecutor)this).ConnectionString;
111: TCommand cmd = new TCommand();
112: cmd.CommandText = cmdText;
113: cmd.Connection = con;
114: if (keyValuePairs != null) {
115: for (int i = 0; i < keyValuePairs.Length - 1; i += 2) {
116: TDataParameter p = new TDataParameter();
117: p.ParameterName = keyValuePairs[i];
118: p.Value = keyValuePairs[i + 1];
119: cmd.Parameters.Add(p);
120: }
121: }
122: cmd.CommandType = CommandType;
123: cmd.CommandTimeout = ((IDbExecutor)this).CommandTimeout;
124: con.Open();
125: return cmd.ExecuteReader(CommandBehavior.CloseConnection);
126: }
127:
128: #endregion
129:
130: }
131: }
只需指定 TConnection, TCommand, TDataParameter 的具体类型,该辅助类并能支持对应工作了;也就是把 SqlServer、MySql 等的具体辅助类合并到了一起并做了实现,真的是一劳永逸啊!相应的,DbExecutor 简化成了:
1: /// <summary>
2: /// 数据访问代理
3: /// </summary>
4: public static class DbExecutor {
5: private static IDbExecutor _files;
6: public static IDbExecutor Files { get { return _files; } private set { _files = value; } }
7: private static IDbExecutor _BK;
8: public static IDbExecutor BK { get { return _BK; } private set { _BK = value; } }
9: private static IDbExecutor _ResourcesProcedure;
10: public static IDbExecutor ResourcesProcedure { get { return _ResourcesProcedure; } private set {
_ResourcesProcedure = value; } }
11: private static IDbExecutor _Resources;
12: public static IDbExecutor Resources { get { return _Resources; } private set { _Resources = value; } }
13: static DbExecutor() {
14: Files = new SqlExecutor<MySqlConnection, MySqlCommand, MySqlParameter>();
15: Files.ConnectionString = System.Configuration.ConfigurationManager.ConnectionStrings["Files"]
.ConnectionString;
16:
17: BK = new SqlExecutor<MySqlConnection, MySqlCommand, MySqlParameter>();
18: BK.ConnectionString = System.Configuration.ConfigurationManager.ConnectionStrings["bk"]
.ConnectionString;
19:
20: Resources = new SqlExecutor<MySqlConnection, MySqlCommand, MySqlParameter>();
21: Resources.ConnectionString = System.Configuration.ConfigurationManager.ConnectionStrings[
"Resources"].ConnectionString;
22:
23:
24: ResourcesProcedure = new SqlExecutor<MySqlConnection, MySqlCommand, MySqlParameter>(
System.Data.CommandType.StoredProcedure);
25: ResourcesProcedure.ConnectionString = System.Configuration.ConfigurationManager
.ConnectionStrings["Resources"].ConnectionString;
26: }
27:
28: }
四、装饰者设计模式的应用
最近,组内希望添加数据库操作人员记录,我只是做了如下的改动便完成了简易日志功能(请参见“装饰者设计模式”):
1: using System;
2: using System.Collections.Generic;
3: using System.Text;
4: using Queen.Data;
5: using MySql.Data.MySqlClient;
6: using System.Web;
7:
8: /// <summary>
9: /// 数据访问代理
10: /// </summary>
11: public static class DbExecutor {
12: private static IDbExecutor _files;
13: public static IDbExecutor Files { get { return _files; } private set { _files = value; } }
14: private static IDbExecutor _BK;
15: public static IDbExecutor BK { get { return _BK; } private set { _BK = value; } }
16: private static IDbExecutor _ResourcesProcedure;
17: public static IDbExecutor ResourcesProcedure { get { return _ResourcesProcedure; }
private set { _ResourcesProcedure = value; } }
18: private static IDbExecutor _Resources;
19: public static IDbExecutor Resources { get { return _Resources; } private set { _Resources = value; } }
20: static DbExecutor() {
21: Files = new SqlExecutor<MySqlConnection, MySqlCommand, MySqlParameter>();
22: Files.ConnectionString = System.Configuration.ConfigurationManager.ConnectionStrings["Files"]
.ConnectionString;
23: Files = new DecorateExecutor(Files);
24:
25: BK = new SqlExecutor<MySqlConnection, MySqlCommand, MySqlParameter>();
26: BK.ConnectionString = System.Configuration.ConfigurationManager.ConnectionStrings["bk"]
.ConnectionString;
27: BK = new DecorateExecutor(BK);
28:
29: Resources = new SqlExecutor<MySqlConnection, MySqlCommand, MySqlParameter>();
30: Resources.ConnectionString = System.Configuration.ConfigurationManager
.ConnectionStrings["Resources"].ConnectionString;
31: Resources = new DecorateExecutor(Resources);
32:
33:
34: ResourcesProcedure = new SqlExecutor<MySqlConnection, MySqlCommand, MySqlParameter>(
System.Data.CommandType.StoredProcedure);
35: ResourcesProcedure.ConnectionString = System.Configuration.ConfigurationManager
.ConnectionStrings["Resources"].ConnectionString;
36: ResourcesProcedure = new DecorateExecutor(ResourcesProcedure);
37: }
38:
39: #region DecorateExecutor, logable
40:
41: class DecorateExecutor : IDbExecutor {
42:
43: private IDbExecutor value;
44:
45: public DecorateExecutor(IDbExecutor value) {
46: this.value = value;
47: }
48:
49: #region IDbExecutor 成员
50:
51: public int CommandTimeout {
52: get {
53: return this.value.CommandTimeout;
54: }
55: set {
56: this.value.CommandTimeout = value;
57: }
58: }
59:
60: public string ConnectionString {
61: get {
62: return this.value.ConnectionString;
63: }
64: set {
65: this.value.ConnectionString = value;
66: }
67: }
68:
69: public int ExecuteNonQuery(string cmdText, params string[] keyValuePairs) {
70: if (null != HttpContext.Current && null != HttpContext.Current.User && null
!= HttpContext.Current.User.Identity && !string.IsNullOrEmpty(HttpContext.Current.User.Identity.Name)) {
71: System.Diagnostics.Trace.WriteLine(string.Format("{0}\t{1} 进行了操作 {2}...",
72: DateTime.Now.ToShortTimeString(), HttpContext.Current.User.Identity.Name,
cmdText.Length > 20 ? cmdText.Substring(0, 20) : cmdText));
73: }
74: return this.value.ExecuteNonQuery(cmdText, keyValuePairs);
75: }
76:
77: public System.Data.IDataReader ExecuteReader(string cmdText, params string[] keyValuePairs) {
78: if (null != HttpContext.Current && null != HttpContext.Current.User && null
!= HttpContext.Current.User.Identity && !string.IsNullOrEmpty(HttpContext.Current.User.Identity.Name)) {
79: System.Diagnostics.Trace.WriteLine(string.Format("{0}\t{1} 进行了操作 {2}...",
80: DateTime.Now.ToShortTimeString(), HttpContext.Current.User.Identity.Name,
cmdText.Length > 20 ? cmdText.Substring(0, 20) : cmdText));
81: }
82: return this.value.ExecuteReader(cmdText, keyValuePairs);
83: }
84:
85: public object ExecuteScalar(string cmdText, params string[] keyValuePairs) {
86: if (null != HttpContext.Current && null != HttpContext.Current.User && null
!= HttpContext.Current.User.Identity && !string.IsNullOrEmpty(HttpContext.Current.User.Identity.Name)) {
87: System.Diagnostics.Trace.WriteLine(string.Format("{0}\t{1} 进行了操作 {2}...",
88: DateTime.Now.ToShortTimeString(), HttpContext.Current.User.Identity.Name,
cmdText.Length > 20 ? cmdText.Substring(0, 20) : cmdText));
89: }
90: return this.value.ExecuteScalar(cmdText, keyValuePairs);
91: }
92:
93: #endregion
94: }
95:
96: #endregion
97:
98: }
五、小结:
优雅的代码让人看着赏心悦目,虽然每个人的审美会有不同,但至少要让自己觉得舒服。我尝试着在编码的过程中积累软件设计的经验,充分发挥面向对象的长处,使得代码的可复用性、可维护性(可调试的能力?)、可扩展性达到极致。在本文前半段,我与大家分享了我在抽象数据库访问行为时的心得(不过是 IDbExecutor 接口),而在接下来的文字里,我使用了泛型编程技巧,使得传统的面向对象(通过继承等手段扩展、复用代码?)得到了看似质的突破,极大缓解了“继承泛滥”的苦恼。
面向过程编程,大量的操作是参数的传递,由于参数之间有逻辑上的关联,且“过程”本身又针对某类关联参数,于是有了“面向对象”。这样一来,人们掌握系统全局的能力得到了加强,从 .net 框架提供的各种服务来看,面向对象实质上降低了编程的门槛,并且提高了人们的生产效率。
通常,除非使用反射,我们很难使得代码不经过扩展而达到“向后兼容”。但反射本身并不优雅,而且执行效率会降低,泛型的推出解决了这样一个矛盾。我个人的理解是,可以将所谓“泛型”看成“类型参数”,传统上,参数基本是基于数据的,而引入了类型参数后,不但不会失去强类型的类型安全特点,还能使得代码被编译器优化,提高执行效率(是指“值类型的装箱、拆箱”操作等)。
但从来没有完美的事,在当真要进行数据库转换、系统迁移的时候,比如从 SqlServer 变为 MySql 时,当前的 IDbExecutor 是不可能让数据库类型透明的:因为开发人员在组织 SQL 语句时必须知道数据库类型,SqlServer、MySql 有各自的 SQL 语言特性,相同的 SQL 语句并不能保证在两个数据库平台上成功运行!解决这个缺陷的办法,除了阉割当前数据库的 SQL 语言特性外(指禁止使用),我看是无解的;但分页操作(关键字 TOP/LIMIT)是无法回避的,而且,在 SqlServer 中参数使用“@p”,而 MySql 中使用“?p”,诸如此类,因此,这个缺陷就是无解的!数据库类型变化时,SQL 语句的改造再所难免,只不过从代码变化的量上,当前的数据访问方式要小得多!
(完)