说正题,假设(只是假设,真的不会这么变态,呵呵)一产品系统要支持ACCESS,SYBASE,SQL SERVER,ORACEL数据库,系统开发完后,要求只改动数据库的连接,也就是说只改动webconfig,来实现系统的无缝切换,其它什么也不改动,可能有的觉得简单,但是要经得起实际检验还是要花点时间测试的,当然写是不算难,见到过有的应用系统开发中,所有的DML语句都以XML形式放在config配置文件里,然后用封装好的数据库访问组件读config配置文件执行SQL语句,开发人员只要建个config文件把增删改查语句写那里就可以,当然这么来有个好处,就是做到了与数据库的无关性了,如果数据库访问组件做的完善的,当然是可以做到系统无缝切换到其它数据库的,当然同时也有缺陷,项目中不仅仅是DML语句吧,有的稍微复杂点的逻辑,存储过程要用下吧,我自己也趋向于喜欢用存储过程,自定义函数来处理稍微复杂的逻辑,而且把DML语句都放在配置文件里我也是不能忍受的,可能是一个个人的习惯吧。
继续,下面开始我要说的利用ADO.NET2.0及以上版本新增的工厂式数据库访问实现应该系统的无缝切换,要实现无缝切换,当然还是要有前提条件了,就是各个不同的数据库之间的表和其它对象都已经成功移植了,没有这个前提,纯用ADO.NET做系统无缝切换那是不可能的了,比如SQL SERVER中写的存储过程,自定义函数直接复制到ORACLE上就行了吗?当然是不行,写法及变量定义要做些调整才可以成功移植的,还有变结构字段类型等等的都可能是要做相应调整,这些都做好了才能谈系统的无缝切换。要做的无缝切换,数据库访问层的代码中最好(并非绝对)不应该出现SqlCommand,SqlDataAdapter,SqlClient,SqlXXX吧,要切换到ORACLE数据上,甲骨文会把你的SqlXXX玩死的,ORACLE里可以OracleCommand,OracleXXX,还有程序执行带参数语句时,比如 where userid=@userid,甲骨文也会玩死你的,oracle里可是where userid=:userid,@前缀在ACCESS,SYBASE,SQL SERVER里是都认得,还有还有字段名的写法问题,ORACLE里可以区分大小写的,比如可能大多习惯这样命名属性和自段,UserName,UserAge,如果在ORACLE里这么命名的话,系统开发过程中的那种痛苦也许只有经历过的人才知道,ORACLE坚持大写为标准,记得很久很久以前的一个夏天的晚上,那时我还是年轻的80后,一位数据库设计比较N的人提到过,尽量在数据库设计和T-SQL编程中采用大写标准,基本上接触的SQL SERVER数据库较多,也习惯了表名,字段名的大写设计,后来发现确实是有道理的。这里提到的问题都是在下面的各个方法中为了兼容不同的数据库需要面对的问题,具体讲到每个执行方法时再具体解释。刚才说SqlCommand,OracleComand都是各自认得,但是DbCommand可是大家都认得的,暂且叫抽象对象吧,还有DbConnection,DbDataAdapter等都是他们都认得的,所以在做支持多数据库访问类库时,就可以用这些对象了,根据这些对象再创建具体对象。ADO.NET2.0中数据库访问工厂中有个 DbProviderFactory 对象,也就是通常说的DataProvider了,正是这个起了关键和方便的作用,是用来创建提供程序对数据源类的实现的实例(就是用来创建实例)。另外数据库操作还要用到参数吧,DbParameter,DbParameterCollection下面都需要用到,先贴一段类库的构造函数,因为共用对象需要先实例化。
public DbConnection conn;//抽象类型
private DbCommand cmd;//抽象类型
private DbProviderFactory provider;
private DbParameter Para;//不同数据库参数类型的抽象类型
private DbDataAdapter Adapter;//对应不同数据库的数据适配器
Dictionary<Type, String> ParametersFormat;//不同数据库参数格式化类型
public string retParaformat = string.Empty;//最终返回的格式化标志,如@{0},:{0}
public DataProviderFactory()
{
//从配置文件中取出标示数据库类型的字符串并通过ProviderName的不同支持不同类型的数据库
string providerName = ConfigurationManager.ConnectionStrings["ConnStr"].ProviderName;//也可以用索引,从1开始
//创建一个数据库对应的实例,使用该实例就可以创建对应的connection,command 和adapater等等对象
provider = DbProviderFactories.GetFactory(providerName);
//创建具体的数据库连接类型和命令执行类型
conn = provider.CreateConnection();
conn.ConnectionString = ConfigurationManager.ConnectionStrings["ConnStr"].ConnectionString;
cmd = provider.CreateCommand();
cmd.Connection = conn;
//创建具体的参数类型
Para = provider.CreateParameter();
//创建具体的适配器类型
Adapter = provider.CreateDataAdapter();
//不同数据库参数前缀格式化
ParametersFormat = new Dictionary<Type, String>();
ParametersFormat.Add(typeof(System.Data.SqlClient.SqlCommand), "@{0}");//因SQL SERVER只返回{0}没有@前缀,在此初始化处理
//返回格式化标志
retParaformat = GetParameterFormat(cmd);
}
上面那段代码中,可以看到我定义了两个公共变量,其中conn在外部只会有一个地方调用它,那就就是执行DataReader方法的时候了,因为大家都知道dr在离开方法体时,连接是不能关闭的,
所以只能在外部调用处显示关闭连接对象 ,必须定义为公共类型了,还有一个公共参数变量是格式化字符串的字符型。前面说到Oracle参数前缀是冒号:,其它几个数据库前缀是@符号,怎么样在切换数据库以后程序能动态识别参数前缀并组合相应的参数变量呢?如果手动写代码用数据库对象类型枚举去一个个判断,那这数据库工厂也没什么意义了,最终找到了一个相当完美的解决方式(微软就是微软,都能替你想到,别人想不强大都难啦,呵呵),其实在做测试的时侯到各不同数据库的参数前缀这就有点犯难了,手写代码一个个处理吧,没问题,觉得应该有简单的方法吧,MS从来就不是傻瓜呀,正好前两天在园子首页就有篇提到这个问题,而且给出相关提示,根据给的提示再gg了一把,终于找到了一个便捷的办法。还是贴方法代码好了,如下:
/// <summary>
/// 根据不同的数据库命令对象返回该类型数据库参数的前缀格式化字符串
/// </summary>
/// <param name="command"></param>
/// <returns></returns>
private string GetParameterFormat(DbCommand command)
{
if (!ParametersFormat.ContainsKey(command.GetType()))
{
this.Open();//读取参数前缀时需打开数据库连接
ParametersFormat.Add(
command.GetType(),
command.Connection.GetSchema("DataSourceInformation")
.Rows[0]["ParameterMarkerFormat"].ToString());
//conn.Close();在真正执行语句的时候去关闭,避免重复打开
}
return ParametersFormat[command.GetType()];
}
就是这个了 ParameterMarkerFormat,即参数标志符格式化,如连接oracle数据库则返回:{0},其它几个数据库返回@{0},惟独SQL SERVER数据库返回{0},到底是MS自己的东西,就是要返回跟别人不一样的东西,也就因为这个,这个类库里很遗憾不得不出现一个SqlCommand,就是上面贴出的构造函数里的初始化那ParametersFormat.Add(typeof(System.Data.SqlClient.SqlCommand), "@{0}");必须这样做下处理,另外包括GetParameterFormat方法里的判断,即不是SQL SERVER数据库时才去读参数前缀,如果是就直接返回@{0},有了这个格式化的前缀字符串,就好办了.那参数名称的赋值就可以类似这样了string.Format("@{0}", ParaName);
下面说说各通用的方法和调用,之前的sqlhelper.cs,oraclehelper.cs,xxhelper.cs中的执行方法大多都很多,有带参数执行的语句的方法,不带参数执行的语句的方法,带参数执行的方法体里面还要循环参数,这些都我都精简掉了,最终演变成了peacehelper.cs(开个玩笑).带参执行和不带参执行DML语句,其实是可以合并成一个方法,各个参数都是保存在数据库命令对象的参数集合中的,我们可以把创建好的命令对象返回给外部程序调用处,调用的地方要带参执行语句的话,就定义参数并赋值就行了,不带参执行的话就不用定义参数了,这么以来就只需要写一个方法就行了,而且执行带掺的语句时不用再循环参数集合了,因为在调用处定义参数时,该参数已经绑定都了DbCommand对象了.写一个返回给外部调用的数据库命令对象的方法,如下:
/// <summary>
/// 抽象参数集合类型
/// </summary>
/// <returns></returns>
public DbParameterCollection GetParmCollection()
{
return cmd.Parameters;
}
添加参数的方法如下:
/// <summary>
/// 添加参数
/// </summary>
/// <param name="ParaName">参数名称</param>
/// <param name="SqlType">参数数据类型</param>
/// <param name="ParaValue">参数值</param>
/// <param name="ParaCollect">参数对象的集合</param>
public void AddParam(string ParaName, DbType SqlType, object ParaValue, DbParameterCollection ParaCollect)
{
//不允许将一个DbCommand对象的Parameters插入到另外一个DbCommand对象,那么多个参数的话可以加上下面一句判断
//如果已经存在至少一个对象时,再深层拷贝一个
if (ParaCollect.Count >= 1)
{
Para = (DbParameter)((ICloneable)ParaCollect[0]).Clone();
}
Para.ParameterName = string.Format(retParaformat, ParaName);
Para.DbType = SqlType;
if (ParaValue == null)
{
Para.Value = string.Empty;//DBNull.Value;
}
else
{
Para.Value = ParaValue;
}
ParaCollect.Add(Para);
}
上面有句判断,如果有多个参数会出异常,网上搜了下,注释就是网上的解释,不多说了,意思很清楚。这个方法里还有一点,如果DbType参数不要的话测试也是可以通过的,猜想如果不显示指定参数数据类型的话,是不是都默认为object类型?这样的话会不会涉及一个装拆箱的操作呢?但是开发人员在调用处添加参数,是不应该关心参数的数据类型才对,干脆数据类型参数不要了,改成如下方法了:
public void AddParam(string ParaName, object ParaValue, DbParameterCollection ParaCollect)
{
if (ParaCollect.Count >= 1)
{
Para = (DbParameter)((ICloneable)ParaCollect[0]).Clone();
}
Para.ParameterName = string.Format(retParaformat, ParaName);//将参数格式化为具体的数据库参数格式
if (ParaValue == null)
{
Para.Value = string.Empty;
}
else
{
Para.Value = ParaValue;
}
ParaCollect.Add(Para);
}
为了兼容不同的数据库(主要是oracle变量特殊问题),添加参数的方法分两种,一种是普通带参执行的DML语句,一种是代参执行的存储过程。对于SQL SERVER数据库即使是存储过程
变量参数仍是@前缀,ORACLE存储过程又是什么前缀呢?很遗憾,ORACLE存储过程的参数变量是不需要任何前缀的,为了单独兼容这一点,对于不同数据库如果调用的存储过程有参数
的话,建议用下面的三个添加参数的方法:
/// <summary>
/// 存储过程输入参数
/// </summary>
/// <param name="ParaName"></param>
/// <param name="ParaValue"></param>
/// <param name="ParaCollect"></param>
public void AddInputParam(string ParaName, object ParaValue, DbParameterCollection ParaCollect)
{
if (ParaCollect.Count >= 1)
{
Para = (DbParameter)((ICloneable)ParaCollect[0]).Clone();
}
Para.ParameterName = string.Format(retParaformat.Replace(":",""), ParaName);//ORACLE存储过程参数前没有冒号
if (ParaValue == null)
{
Para.Value = string.Empty;
}
else
{
Para.Value = ParaValue;
}
ParaCollect.Add(Para);
}
/// <summary>
/// 存储过程输出参数
/// </summary>
/// <param name="ParaName"></param>
/// <param name="ParaValue"></param>
/// <param name="ParaCollect"></param>
public void AddOutputParam(string ParaName, DbParameterCollection ParaCollect)
{
if (ParaCollect.Count >= 1)
{
Para = (DbParameter)((ICloneable)ParaCollect[0]).Clone();
}
Para.ParameterName = string.Format(retParaformat.Replace(":", ""), ParaName);
Para.Value = string.Empty;
ParaCollect.Add(Para);
ParaCollect[Para.ParameterName].Direction = System.Data.ParameterDirection.Output;//指定该参数为输出参数
}
/// <summary>
/// 存储过程返回值参数
/// </summary>
/// <param name="ParaName"></param>
/// <param name="ParaValue"></param>
/// <param name="ParaCollect"></param>
public void AddReturnParam(string ParaName,DbParameterCollection ParaCollect)
{
if (ParaCollect.Count >= 1)
{
Para = (DbParameter)((ICloneable)ParaCollect[0]).Clone();
}
Para.ParameterName = string.Format(retParaformat.Replace(":", ""), ParaName);
Para.Value = string.Empty;
ParaCollect.Add(Para);
ParaCollect[Para.ParameterName].Direction = System.Data.ParameterDirection.ReturnValue;//指定该参数为返回值参数
}
OK,现在开始说下peacehelper.cs里的八大方法(其实算起来应该是10个),应该来说涵盖绝大多应该系统操作数据库的绝大部分功能,如果有特殊的操作可以在此基础上添加。
第一个,大家都熟悉的返回结果集:
/// <summary>
/// 执行SQL并返回数据集
/// </summary>
/// <param name="sql"></param>
/// <returns></returns>
public DataSet ExecDataSet(string Sql)
{
DataSet ds = new DataSet();
try
{
this.Open();
cmd.CommandText = Replace(Sql);
Adapter.SelectCommand = cmd;
Adapter.Fill(ds);
}
catch (Exception ex)
{
throw ex;
}
finally
{
this.Close();
}
return ds;
}
上面的方法大家看了是不是觉得既简单又熟悉,确实是的,但仍然相当以前的xxhelper.cs里是做了简化的,该方法既可以直接执行不带参DML语句,也可以执行带参的,但是该方法的形参却
只有一个,之前的xxhelper.cs里带参执行的话,形参中大多至少还另外一个形参的,比如SqlPeremeters[]类型或参数集合类型的形参,而且方法体里面大多会循环读取参数,上面的方法里
却没有,都简化掉了,唯一多了一点的是,所执行的命令语句执行前要做一个特殊字符替换,cmd.CommandText = Replace(Sql),Replace方法主要是替换参数前缀,Replace方法如下:
/// <summary>
/// 替换DML语句里的参数前缀
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public string Replace(string str)
{
return str.Replace("$", retParaformat.Substring(0, 1));
}
因为不同数据库除了在添加参数时有前缀的区别,再具体执行语句时也有前缀区别嘛,比如SQL SERVER里 SELECT USER_NAME,USER_AGE FROM USERS WHERE USER_ID=@USER_ID,ORACLE里是这样的SELECT USER_NAME,USER_AGE FROM USERS WHERE USER_ID=:USER_ID,在此就需要统一一个前缀规则了,统一这样SELECT USER_NAME,USER_AGE FROM USERS WHERE USER_ID=$USER_ID,在执行前根据不同数据库替换前缀$符号,当然这个约定规则不一定是最完美的,也许还存在一定的问题,写到这我也突然想起来之前我见过别人
的系统中有的就是变量参数用的这种类似特殊符号,肯定也是为了兼容多数据库所作的处理了,呵呵,具体的调用及测试之后统一说明。还有几个方法也和上面类似,大家都熟悉的。如下(不再做具体解释了):
/// <summary>
/// 执行SQL语句并返回DataReader对象
/// </summary>
/// <param name="dbcon"></param>
/// <param name="cmdText"></param>
/// <returns></returns>
public DbDataReader ExecuteDataReader(DbConnection dbcon,string cmdText)
{
try
{
if (dbcon.State == ConnectionState.Closed)
{
dbcon.Open();
}
cmd.CommandText = Replace(cmdText);
DbDataReader dr = cmd.ExecuteReader();
cmd.Parameters.Clear();
cmd.Dispose();
return dr;
}
catch
{
dbcon.Close();//发生异常在此处关闭,否则在调用显式处关闭
return null;
}
} /// <summary>
/// 判断记录是否存在
/// </summary>
/// <param name="Sql"></param>
/// <returns></returns>
public bool Exist(string Sql)
{
bool exist;
this.Open();
cmd.CommandText = Replace(Sql);
DbDataReader dr = cmd.ExecuteReader();
if (dr.HasRows)
{
exist = true; //记录存在
}
else
{
exist = false; //记录不存在
}
dr.Close();
this.Close();
return exist;
}
/// <summary>
/// 执行SQL语句
/// </summary>
/// <param name="sql"></param>
public void ExecSql(string Sql)
{
try
{
this.Open();
cmd.CommandText = Replace(Sql);
cmd.ExecuteNonQuery();
cmd.Dispose();
}
catch (Exception ex)
{
throw ex;
}
finally
{
this.Close();
} }
/// <summary>
/// 执行SQL语句,返回一个单值
/// </summary>
/// <param name="sql"></param>
/// <returns></returns>
public string ReturnValue(string Sql)
{
object returnValue = string.Empty;
try
{
this.Open();
cmd.CommandText = Replace(Sql);
returnValue = cmd.ExecuteScalar();
if (returnValue == null)
{
returnValue = string.Empty;
}
}
catch (Exception ex)
{
throw ex;
}
finally
{
this.Close();
}
return returnValue.ToString();
}
/// <summary>
/// 执行多条SQL语句并启用数据库事务
/// </summary>
/// <param name="SQLStringList"></param>
public bool ExecSqlTran(List<String> SQLStringList)
{
this.Open();
DbTransaction trans = conn.BeginTransaction();
cmd.Transaction = trans;
try
{
for (int n = 0; n < SQLStringList.Count; n++)
{
cmd.CommandText = Replace(SQLStringList[n]);
cmd.ExecuteNonQuery();
}
trans.Commit();
return true;
}
catch
{
trans.Rollback();
return false;
}
finally
{
this.Close();
}
}
下面说下两个存储过程,存储过程基本上分两种,返回结果集的存储过程和执行业务逻辑不返回结果集但却有返回值(如标志等),对于需要有返回值的存储过程,我个人趋向于用输出
参数代替返回值,因为都能达到一样的效果目的,而且输出参数可以有多个,也就可以根据需要能有多个所谓的“返回值”,所以我之前的开发中一直是用output参数来代替return参数。
/// <summary>
/// 执行存储过程并返回结果集
/// </summary>
/// <param name="storedProcName">存储过程名</param>
/// <returns>DataSet</returns>
public DataSet RunProcedure(string storedProcName)
{
DataSet ds = new DataSet();
try
{
this.Open();
cmd.CommandText = storedProcName;
cmd.CommandType = CommandType.StoredProcedure;
Adapter.SelectCommand = cmd;
//Adapter.SelectCommand.CommandTimeout = 1200;//可以设置适当的超时时间(秒),避免选择时间段过大导致填充数据集超时
Adapter.Fill(ds);
}
catch (Exception ex)
{
throw ex;
}
finally
{
this.Close();
}
return ds;
}
/// <summary>
/// 执行存储过程,方法不返回结果集
/// </summary>
/// <param name="storedProcName"></param>
public void RunVoidProcedure(string storedProcName)
{
cmd.CommandText = storedProcName;
cmd.CommandType = CommandType.StoredProcedure;
try
{
this.Open();
cmd.ExecuteNonQuery();
}
catch (Exception ex)
{
throw ex;
}
finally
{
this.Close();
}
}
下面说两个反射方法,测试之后为了方便调用,减少操作添加的,一个是把实体类的属性转换为参数,另一个是把从数据库取出的某条记录转换为实体类,这两个还是非常有用,尤其是在系统开发时调用比较方便,以前我是见到反射就绕道走的,这次算是第一次用反射,发现确实是很方便。如下:
/// <summary>
/// 将实体类的属性进行参数转换(ORACLE测试通不过,必须要求所有参数都包含在语句中才行)
/// </summary>
/// <param name="model"></param>
/// <param name="ParaCollect"></param>
//public void ConvertToParameters(object model, DbParameterCollection ParaCollect)
//{
// Type T = model.GetType();
// PropertyInfo[] propert = T.GetProperties();
// for (int i = 0; i < propert.Length; i++)
// {
// AddParam(propert[i].Name, propert[i].GetValue(model, null), ParaCollect);
// }
//}
/// <summary>
/// 将实体类的属性进行参数转换
/// </summary>
/// <param name="model"></param>
/// <param name="ParaCollect"></param>
public void ConvertToParameters(object model, DbParameterCollection ParaCollect,List<string> fields)
{
Type T = model.GetType();
PropertyInfo[] propert = T.GetProperties();
for (int i = 0; i < propert.Length; i++)
{
if (fields.Contains(propert[i].Name)) //检测必须参数化的实体属性
{
AddParam(propert[i].Name, propert[i].GetValue(model, null), ParaCollect);
}
}
} /// <summary>
/// 通过反射将取出的数据写入实体类(ORACLE测试通不过,需进行类型强制转换)
/// </summary>
/// <param name="model"></param>
/// <param name="cmdText"></param>
//public void GetModel(object model, string cmdText)
//{
// PropertyInfo propertyInfo;
// DbDataReader dr = ExecuteDataReader(conn, cmdText);
// while (dr.Read())
// {
// for (int i = 0; i < dr.FieldCount; i++)
// {
// propertyInfo = model.GetType().GetProperty(dr.GetName(i));
// if (propertyInfo != null)
// {
// if (dr.GetValue(i) != DBNull.Value)
// {
// //Type t = dr.GetValue(i).GetType();
// propertyInfo.SetValue(model, dr.GetValue(i), null);
// }
// }
// }
// }
// dr.Close();
// conn.Close();
//}
/// <summary>
/// 通过反射将数据绑定到实体对象,由于不同数据库对应于.NET的数据类型不一样
/// 需做强制类型转换
/// </summary>
/// <param name="model"></param>
/// <param name="cmdText"></param>
public void GetModel(object model, string cmdText)
{
PropertyInfo propertyInfo;
DbDataReader dr = ExecuteDataReader(conn, cmdText);
object _value;
while (dr.Read())
{
for (int i = 0; i < dr.FieldCount; i++)
{
propertyInfo = model.GetType().GetProperty(dr.GetName(i));
if (propertyInfo != null && dr.GetValue(i) != DBNull.Value)
{
switch (propertyInfo.PropertyType.ToString())
{
case "System.String":
{
_value = Convert.ToString(dr.GetValue(i));//字符串是全球通用类型,也可以不用转换
propertyInfo.SetValue(model, _value, null);
}break;
case "System.Int32":
{
_value = Convert.ToInt32(dr.GetValue(i));
propertyInfo.SetValue(model, _value, null);
} break;
case "System.Single":
{
_value = Convert.ToSingle(dr.GetValue(i));
propertyInfo.SetValue(model, _value, null);
} break;
case "System.Decimal":
{
_value = Convert.ToDecimal(dr.GetValue(i));
propertyInfo.SetValue(model, _value, null);
} break;
case "System.Double":
{
_value = Convert.ToDouble(dr.GetValue(i));
propertyInfo.SetValue(model, _value, null);
} break;
case "":
{
_value = Convert.ToDateTime(dr.GetValue(i));
propertyInfo.SetValue(model, _value, null);
} break;
default: break;
}
}
}
}
dr.Close();
conn.Close();
}
从上面的注释掉的方法对比中可以看到为了兼容不同的数据库,必须要做额外的处理,比如类型转换,SQL SERVER的int 对应ORALCE的number,UserInfo的字段属性UserAge定义的是int类型,连接ORALCE时,.NET识别number类型为System.Decimal,把Decimal赋值给Int32当然是不行的,所以得做强制转换才行。还有一点要注意下,就是将数据绑定到实体对象时,由于ORACLE坚持大写标准和解析机制,如果属性名和字段名大小写不一致的话,propertyInfo = model.GetType().GetProperty(dr.GetName(i)) ,propertyInfo 始终是null值,比如SELECT UserName,UserAge FROM USER_TEST WHERE USERID=$USERID,SQL SERVER 执行的时候调试可以看到dr.GetName(0)是UserName,dr.GetName(1)是UserAge,ORACLE执行解析就变了,全是大写了,变成了USERNAE,USERAGE,这么一来和找不到UserInfo类的属性了,因为UserInfo类的属性是 UserName,和UserAge,C#语言变量也是区分大小写的嘛,当然就找不到了,所以propertyInfo就为null了,故在这里再次建议大家在数据库设计和程序字段属性设计时采用大写标准(如果不涉及多数据库当然也不需要这么做)。
最后说下测试调用代码,首先webconfig配置里面这样配置下,主要选取SQL SERVER和ORACLE做测试,毕竟这是.NET支持的两个典型数据库,要是把.NET所支持的所有书库都测试一遍,那测试量可不小了,呵呵。
<connectionStrings>
<add name="ConnStr" connectionString="uid=sa;pwd=peace;database=TEST;server=." providerName="System.Data.SqlClient" />
<!--<add name="ConnStr" connectionString="server=.;data source=peace;user id=cct;password=cct;enlist=true" providerName="System.Data.OracleClient"/>-->
</connectionStrings>
protected void Page_Load(object sender, EventArgs e)
{
//测试DataReader,SQLSERVER和ORACLE都通过
//DataProviderFactory fac = new DataProviderFactory();
//DbParameterCollection ParaCollect = fac.GetParmCollection();
//fac.AddParam("USERID", 100, ParaCollect);
//DbDataReader dr = fac.ExecuteDataReader(fac.conn, "SELECT * FROM USER_TEST WHERE USERID=$USERID");
//while (dr.Read())
//{
// string a = dr[1].ToString();
//}
//fac.conn.Close();//在调用处显示关闭 //无参数DataSet测试 SQLSERVER和ORACLE都通过
//DataTable dt = fac.ExecDataSet("SELECT * FROM USER_TEST").Tables[0];
//带参数DataSet测试 SQLSERVER和ORACLE都通过
//DbParameterCollection ParaCollect = fac.GetParmCollection();
//fac.AddParam("USERID", 100, ParaCollect);
//fac.AddParam("USERNAME", "局%", ParaCollect);//这里的参数名可以任意成其它,不一定非要和字段名相同(下同)
//DataTable dt = fac.ExecDataSet("SELECT * FROM USER_TEST WHERE USERNAME LIKE $USERNAME").Tables[0];
//DataTable dt = fac.ExecDataSet("SELECT * FROM USER_TEST WHERE USERID=$USERID OR USERNAME LIKE $USERNAME").Tables[0];//多参数测试
//单值测试(带参数) SQLSERVER和ORACLE都通过
//DbParameterCollection ParaCollect = fac.GetParmCollection();
//fac.AddParam("USERID", 100, ParaCollect);
//string retValue = fac.ReturnValue("SELECT USERNAME FROM USER_TEST WHERE USERID=$USERID");
//带参存储过程测试返回结果集 SQLSERVER和ORACLE都通过
//DbParameterCollection ParaCollect = fac.GetParmCollection();
//fac.AddParam("StartDate", "2009-8-1", ParaCollect);
//fac.AddParam("EndDate", "2009-8-21", ParaCollect);
//DataTable dt = fac.RunProcedure("USP_GetMixedReport").Tables[0]; //带参数测试存储过程的输出参数值和返回值,方法不返回结果集 SQLSERVER通过
//int flag = 0, sign = 0, ret = 0;
//DbParameterCollection ParaCollect = fac.GetParmCollection();
//fac.AddParam("USER_ACCOUNT", DbType.String, "admin", ParaCollect);
//fac.AddParam("USER_PWD", DbType.String, "68053af2923e00204c3ca7c6a3150cf7", ParaCollect);
//fac.AddParam("FLAG", DbType.Int32, "", ParaCollect);
//ParaCollect["@FLAG"].Direction = System.Data.ParameterDirection.Output;
//fac.AddParam("SIGN", DbType.Int32, "", ParaCollect);
//ParaCollect["@SIGN"].Direction = System.Data.ParameterDirection.Output;
//fac.AddParam("RetValue", DbType.String, "", ParaCollect);
//ParaCollect["@RetValue"].Direction = System.Data.ParameterDirection.ReturnValue;
//fac.RunVoidProcedure("SP_ValideLogin");
//flag = int.Parse(ParaCollect["@FLAG"].Value.ToString());
//sign = int.Parse(ParaCollect["@SIGN"].Value.ToString());
//ret = int.Parse(ParaCollect["@RetValue"].Value.ToString());//存储过程约定返回值必须是int型 //改进后带参数测试存储过程的输出参数值和返回值的测试 SQLSERVER和ORACLE都通过
//int flag = 0, sign = 0, ret = 0;
//DataProviderFactory fac = new DataProviderFactory();
//DbParameterCollection ParaCollect = fac.GetParmCollection();
//fac.AddInputParam("USER_ACCOUNT", "admin", ParaCollect);
//fac.AddInputParam("USER_PWD", "68053af2923e00204c3ca7c6a3150cf7", ParaCollect);
//fac.AddOutputParam("FLAG", ParaCollect);
//fac.AddOutputParam("SIGN", ParaCollect);
//fac.AddReturnParam("RetValue", ParaCollect);
//fac.RunVoidProcedure("SP_ValideLogin");
//string prefix = fac.retParaformat.Replace(":","");//Oracle存储过程参数前冒号移除掉
//flag = int.Parse(ParaCollect[string.Format(prefix,"FLAG")].Value.ToString());
//sign = int.Parse(ParaCollect[string.Format(prefix, "SIGN")].Value.ToString());
//ret = int.Parse(ParaCollect[string.Format(prefix, "RetValue")].Value.ToString());//存储过程约定返回值必须是int型 //调用存储过程测试 SQLSERVER和ORACLE都通通过
//DataProviderFactory fac = new DataProviderFactory();
//DbParameterCollection ParaCollect = fac.GetParmCollection();
//fac.AddInputParam("P_UserID", 7, ParaCollect);
//fac.AddInputParam("P_UserName", "peace", ParaCollect);
//fac.AddInputParam("P_UserAge", 100, ParaCollect);
//fac.RunVoidProcedure("PROC_USER_TEST_ADD");
//多条提交事务处理测试 SQLSERVER和ORACLE都通过
//List<string> SqlList = new List<string>();
//DataProviderFactory fac = new DataProviderFactory();
//DbParameterCollection ParaCollect = fac.GetParmCollection();
//fac.AddParam("UserName", "peaceli", ParaCollect);
//fac.AddParam("UserAge", 150, ParaCollect);
//SqlList.Add("INSERT INTO USER_TEST(UserName,UserAge) VALUES($UserName,$UserAge)");
//SqlList.Add("INSERT INTO USER_TEST(UserName,UserAge) VALUES($UserName,$UserAge)");
//SqlList.Add("INSERT INTO USER_TEST(UserName,UserAge) VALUES($UserName,$UserAge)");
//fac.ExecSqlTran(SqlList);
//插入操作参数测试(SQL SERVER) 通过
//UserInfo ui = new UserInfo();
//ui.UserName = "hello peace";
//ui.UserAge = 100;
//Addinn(ui);
//插入操作参数测试(Oracle) 通过
//UserInfo ui = new UserInfo();
//ui.USERID = 10;
//ui.USERNAME = "hello peace";
//ui.USERAGE = 120;
//Addin(ui);
//插入操作反射参数转换测试 SQLSERVER和ORACLE都通过
//UserInfo ui = new UserInfo();
//ui.USERNAME = "peaceli";
//ui.USERAGE = 110;
//Add(ui);
//返回实体对象测试 SQLSERVER和ORACLE都通过
UserInfo ui = new UserInfo();
ui.USERID = 1;
GetInfo(ui);
}
//private void Addinn(UserInfo ui)
//{
// DataProviderFactory fac = new DataProviderFactory();
// DbParameterCollection ParaCollect = fac.GetParmCollection();
// fac.AddParam("@UserName", ui.UserName, ParaCollect);
// fac.AddParam("@UserAge", ui.UserAge, ParaCollect);
// fac.ExecSql("INSERT INTO USER_TEST(UserName,UserAge) VALUES(@UserName,@UserAge)");
//}
private void Addin(UserInfo ui)
{
DataProviderFactory fac = new DataProviderFactory();
DbParameterCollection ParaCollect = fac.GetParmCollection();
//fac.AddParam(":UserName", ui.UserName, ParaCollect);//给参数赋值时冒号可以不加,但有的版本可能必须加
//fac.AddParam(":UserAge", ui.UserAge, ParaCollect);
//fac.AddParam("UserID", ui.USERID, ParaCollect); //这行注释放开在ORACLE下同不过,ORACLE要求所全参数匹配,有多余参数就不行,这点有些变态
fac.AddParam("UserName", ui.USERNAME, ParaCollect);//SQL SERVER只要求用到的参数包含在参数集合里就行了,其它多余参数并不影响执行
fac.AddParam("UserAge", ui.USERAGE, ParaCollect);
fac.ExecSql("INSERT INTO USER_TEST(UserName,UserAge) VALUES(:UserName,:UserAge)");
}
private void Add(UserInfo ui)
{
DataProviderFactory fac = new DataProviderFactory();
DbParameterCollection ParaCollect = fac.GetParmCollection();
string[] fields = { "USERNAME", "USERAGE" };//要求参数化的实体属性
List<string> ListFields = new List<string>(fields);
fac.ConvertToParameters(ui, ParaCollect, ListFields);//如果新增记录有很多参数的话,可能AddParam很多次,采用反射批量转换
fac.ExecSql("INSERT INTO USER_TEST(USERNAME,USERAGE) VALUES($USERNAME,$USERAGE)");
}
private void GetInfo(UserInfo ui)
{
DataProviderFactory fac = new DataProviderFactory();
DbParameterCollection ParaCollect = fac.GetParmCollection();
fac.AddParam("USERID", ui.USERID, ParaCollect);
fac.GetModel(ui, "SELECT USERNAME,USERAGE FROM USER_TEST WHERE USERID=$USERID");
}
}
UserInfo类如下:
public class UserInfo
{
public int USERID { get; set; }
public string USERNAME { get; set; }
public int USERAGE { get; set; }
}
测试到最后类属性改动过,统一改成了大写,再次建议大写标准(包括数据库设计),可以定义成USER_ID,USER_NAME,USER_AGE等,并与数据库字段名保持一致,这样有利于多数据库的
兼容。
结语:个人并不反对项目里单独用对应的xxhelper.cs,某个项目用SQLSERVER数据库,就用SqlHelper.csL类,ORACLE就用OracleHelper.cs类,这样来得更干脆快捷,基本上每个项目都是这对特定的数据库在开发,没必要搞成通用类,真要搞成通用类,要经过大量的实际测试,也许我最近有时寂寞空虚也无聊,突然想测试下同时也想改进下,呵呵,零零碎碎花了点时间测试了下,选取两个数据库测试了一遍,最终只需要改动config配置的数据库连接就可以了,真正达到了一套系统的无缝切换。里面有些可能还说的不够准确,可能也还有遗漏的地方,仅供参考吧!!!
访问类库的文件完整的贴一次,如下:
//*****************************************************************************************************************
//* 编写人 :peace
//* EMAIL : peacechzh@126.com
//* 开发日期:2009-10-21
//* 修 改 人:
//* 修改日期:
//* 描 述:数据库工厂访问类
//* 更新描述:里面供调用执行的各方法可带参数执行,在外部指定参数名和参数值即可。
//* 最终期望:支持.NET所支持的所有数据库并达到系统的无缝切换(尽情的忽悠吧O(∩_∩)O~)
//*****************************************************************************************************************
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Data.Common;
using System.Configuration;
using System.Reflection;
namespace DataProvider
{
public class DataProviderFactory
{
public DbConnection conn;//抽象类型
private DbCommand cmd;//抽象类型
private DbProviderFactory provider;
private DbParameter Para;//不同数据库参数类型的抽象类型
private DbDataAdapter Adapter;//对应不同数据库的数据适配器
Dictionary<Type, String> ParametersFormat;//不同数据库参数格式化类型
public string retParaformat = string.Empty;//最终返回的格式化标志,如@{0},:{0}
public DataProviderFactory()
{
//从配置文件中取出标示数据库类型的字符串并通过ProviderName的不同支持不同类型的数据库
string providerName = ConfigurationManager.ConnectionStrings["ConnStr"].ProviderName;//也可以用索引,从1开始
//创建一个数据库对应的实例,使用该实例就可以创建对应的connection,command 和adapater等等对象
provider = DbProviderFactories.GetFactory(providerName);
//创建具体的数据库连接类型和命令执行类型
conn = provider.CreateConnection();
conn.ConnectionString = ConfigurationManager.ConnectionStrings["ConnStr"].ConnectionString;
cmd = provider.CreateCommand();
cmd.Connection = conn;
//创建具体的参数类型
Para = provider.CreateParameter();
//创建具体的适配器类型
Adapter = provider.CreateDataAdapter();
//不同数据库参数前缀格式化
ParametersFormat = new Dictionary<Type, String>();
ParametersFormat.Add(typeof(System.Data.SqlClient.SqlCommand), "@{0}");//因SQL SERVER只返回{0}没有@前缀,在此初始化处理
//返回格式化标志
retParaformat = GetParameterFormat(cmd);
}
/// <summary>
/// 添加参数
/// </summary>
/// <param name="ParaName">参数名称</param>
/// <param name="SqlType">参数数据类型</param>
/// <param name="ParaValue">参数值</param>
/// <param name="ParaCollect">参数对象的集合</param>
public void AddParam(string ParaName, DbType SqlType, object ParaValue, DbParameterCollection ParaCollect)
{
//不允许将一个DbCommand对象的Parameters插入到另外一个DbCommand对象,那么多个参数的话可以加上下面一句判断
//如果已经存在至少一个对象时,再深层拷贝一个
if (ParaCollect.Count >= 1)
{
Para = (DbParameter)((ICloneable)ParaCollect[0]).Clone();
}
Para.ParameterName = string.Format(retParaformat, ParaName);
Para.DbType = SqlType;
if (ParaValue == null)
{
Para.Value = string.Empty;//DBNull.Value;
}
else
{
Para.Value = ParaValue;
}
ParaCollect.Add(Para);
}
public void AddParam(string ParaName, object ParaValue, DbParameterCollection ParaCollect)
{
if (ParaCollect.Count >= 1)
{
Para = (DbParameter)((ICloneable)ParaCollect[0]).Clone();
}
Para.ParameterName = string.Format(retParaformat, ParaName);//将参数格式化为具体的数据库参数格式
if (ParaValue == null)
{
Para.Value = string.Empty;
}
else
{
Para.Value = ParaValue;
}
ParaCollect.Add(Para);
}
/// <summary>
/// 存储过程输入参数
/// </summary>
/// <param name="ParaName"></param>
/// <param name="ParaValue"></param>
/// <param name="ParaCollect"></param>
public void AddInputParam(string ParaName, object ParaValue, DbParameterCollection ParaCollect)
{
if (ParaCollect.Count >= 1)
{
Para = (DbParameter)((ICloneable)ParaCollect[0]).Clone();
}
Para.ParameterName = string.Format(retParaformat.Replace(":",""), ParaName);//ORACLE存储过程参数前没有冒号
if (ParaValue == null)
{
Para.Value = string.Empty;
}
else
{
Para.Value = ParaValue;
}
ParaCollect.Add(Para);
}
/// <summary>
/// 存储过程输出参数
/// </summary>
/// <param name="ParaName"></param>
/// <param name="ParaValue"></param>
/// <param name="ParaCollect"></param>
public void AddOutputParam(string ParaName, DbParameterCollection ParaCollect)
{
if (ParaCollect.Count >= 1)
{
Para = (DbParameter)((ICloneable)ParaCollect[0]).Clone();
}
Para.ParameterName = string.Format(retParaformat.Replace(":", ""), ParaName);
Para.Value = string.Empty;
ParaCollect.Add(Para);
ParaCollect[Para.ParameterName].Direction = System.Data.ParameterDirection.Output;//指定该参数为输出参数
}
/// <summary>
/// 存储过程返回值参数
/// </summary>
/// <param name="ParaName"></param>
/// <param name="ParaValue"></param>
/// <param name="ParaCollect"></param>
public void AddReturnParam(string ParaName,DbParameterCollection ParaCollect)
{
if (ParaCollect.Count >= 1)
{
Para = (DbParameter)((ICloneable)ParaCollect[0]).Clone();
}
Para.ParameterName = string.Format(retParaformat.Replace(":", ""), ParaName);
Para.Value = string.Empty;
ParaCollect.Add(Para);
ParaCollect[Para.ParameterName].Direction = System.Data.ParameterDirection.ReturnValue;//指定该参数为返回值参数
}
/// <summary>
/// 抽象参数集合类型
/// </summary>
/// <returns></returns>
public DbParameterCollection GetParmCollection()
{
return cmd.Parameters;
}
/// <summary>
/// 执行SQL并返回数据集
/// </summary>
/// <param name="sql"></param>
/// <returns></returns>
public DataSet ExecDataSet(string Sql)
{
DataSet ds = new DataSet();
try
{
this.Open();
cmd.CommandText = Replace(Sql);
Adapter.SelectCommand = cmd;
Adapter.Fill(ds);
}
catch (Exception ex)
{
throw ex;
}
finally
{
this.Close();
}
return ds;
}
/// <summary>
/// 执行SQL语句并返回DataReader对象
/// </summary>
/// <param name="dbcon"></param>
/// <param name="cmdText"></param>
/// <returns></returns>
public DbDataReader ExecuteDataReader(DbConnection dbcon,string cmdText)
{
try
{
if (dbcon.State == ConnectionState.Closed)
{
dbcon.Open();
}
cmd.CommandText = Replace(cmdText);
DbDataReader dr = cmd.ExecuteReader();
cmd.Parameters.Clear();
cmd.Dispose();
return dr;
}
catch
{
dbcon.Close();//发生异常在此处关闭,否则在调用显式处关闭
return null;
}
} /// <summary>
/// 判断记录是否存在
/// </summary>
/// <param name="Sql"></param>
/// <returns></returns>
public bool Exist(string Sql)
{
bool exist;
this.Open();
cmd.CommandText = Replace(Sql);
DbDataReader dr = cmd.ExecuteReader();
if (dr.HasRows)
{
exist = true; //记录存在
}
else
{
exist = false; //记录不存在
}
dr.Close();
this.Close();
return exist;
}
/// <summary>
/// 执行SQL语句
/// </summary>
/// <param name="sql"></param>
public void ExecSql(string Sql)
{
try
{
this.Open();
cmd.CommandText = Replace(Sql);
cmd.ExecuteNonQuery();
cmd.Dispose();
}
catch (Exception ex)
{
throw ex;
}
finally
{
this.Close();
} }
/// <summary>
/// 执行SQL语句,返回一个单值
/// </summary>
/// <param name="sql"></param>
/// <returns></returns>
public string ReturnValue(string Sql)
{
object returnValue = string.Empty;
try
{
this.Open();
cmd.CommandText = Replace(Sql);
returnValue = cmd.ExecuteScalar();
if (returnValue == null)
{
returnValue = string.Empty;
}
}
catch (Exception ex)
{
throw ex;
}
finally
{
this.Close();
}
return returnValue.ToString();
}
/// <summary>
/// 执行多条SQL语句并启用数据库事务
/// </summary>
/// <param name="SQLStringList"></param>
public bool ExecSqlTran(List<String> SQLStringList)
{
this.Open();
DbTransaction trans = conn.BeginTransaction();
cmd.Transaction = trans;
try
{
for (int n = 0; n < SQLStringList.Count; n++)
{
cmd.CommandText = Replace(SQLStringList[n]);
cmd.ExecuteNonQuery();
}
trans.Commit();
return true;
}
catch
{
trans.Rollback();
return false;
}
finally
{
this.Close();
}
} /// <summary>
/// 执行存储过程并返回结果集
/// </summary>
/// <param name="storedProcName">存储过程名</param>
/// <returns>DataSet</returns>
public DataSet RunProcedure(string storedProcName)
{
DataSet ds = new DataSet();
try
{
this.Open();
cmd.CommandText = storedProcName;
cmd.CommandType = CommandType.StoredProcedure;
Adapter.SelectCommand = cmd;
//Adapter.SelectCommand.CommandTimeout = 1200;//可以设置适当的超时时间(秒),避免选择时间段过大导致填充数据集超时
Adapter.Fill(ds);
}
catch (Exception ex)
{
throw ex;
}
finally
{
this.Close();
}
return ds;
}
/// <summary>
/// 执行存储过程,方法不返回结果集
/// </summary>
/// <param name="storedProcName"></param>
public void RunVoidProcedure(string storedProcName)
{
cmd.CommandText = storedProcName;
cmd.CommandType = CommandType.StoredProcedure;
try
{
this.Open();
cmd.ExecuteNonQuery();
}
catch (Exception ex)
{
throw ex;
}
finally
{
this.Close();
}
} /// <summary>
/// 将实体类的属性进行参数转换(ORACLE测试通不过,必须要求所有参数都包含在语句中才行)
/// </summary>
/// <param name="model"></param>
/// <param name="ParaCollect"></param>
//public void ConvertToParameters(object model, DbParameterCollection ParaCollect)
//{
// Type T = model.GetType();
// PropertyInfo[] propert = T.GetProperties();
// for (int i = 0; i < propert.Length; i++)
// {
// AddParam(propert[i].Name, propert[i].GetValue(model, null), ParaCollect);
// }
//}
/// <summary>
/// 将实体类的属性进行参数转换
/// </summary>
/// <param name="model"></param>
/// <param name="ParaCollect"></param>
public void ConvertToParameters(object model, DbParameterCollection ParaCollect,List<string> fields)
{
Type T = model.GetType();
PropertyInfo[] propert = T.GetProperties();
for (int i = 0; i < propert.Length; i++)
{
if (fields.Contains(propert[i].Name)) //检测必须参数化的实体属性
{
AddParam(propert[i].Name, propert[i].GetValue(model, null), ParaCollect);
}
}
} /// <summary>
/// 通过反射将取出的数据写入实体类(ORACLE测试通不过,需进行类型强制转换)
/// </summary>
/// <param name="model"></param>
/// <param name="cmdText"></param>
//public void GetModel(object model, string cmdText)
//{
// PropertyInfo propertyInfo;
// DbDataReader dr = ExecuteDataReader(conn, cmdText);
// while (dr.Read())
// {
// for (int i = 0; i < dr.FieldCount; i++)
// {
// propertyInfo = model.GetType().GetProperty(dr.GetName(i));
// if (propertyInfo != null)
// {
// if (dr.GetValue(i) != DBNull.Value)
// {
// //Type t = dr.GetValue(i).GetType();
// propertyInfo.SetValue(model, dr.GetValue(i), null);
// }
// }
// }
// }
// dr.Close();
// conn.Close();
//}
/// <summary>
/// 通过反射将数据绑定到实体对象,由于不同数据库对应于.NET的数据类型不一样
/// 需做强制类型转换
/// </summary>
/// <param name="model"></param>
/// <param name="cmdText"></param>
public void GetModel(object model, string cmdText)
{
PropertyInfo propertyInfo;
DbDataReader dr = ExecuteDataReader(conn, cmdText);
object _value;
while (dr.Read())
{
for (int i = 0; i < dr.FieldCount; i++)
{
propertyInfo = model.GetType().GetProperty(dr.GetName(i));
if (propertyInfo != null && dr.GetValue(i) != DBNull.Value)
{
switch (propertyInfo.PropertyType.ToString())
{
case "System.String":
{
_value = Convert.ToString(dr.GetValue(i));//字符串是全球通用类型,也可以不用转换
propertyInfo.SetValue(model, _value, null);
}break;
case "System.Int32":
{
_value = Convert.ToInt32(dr.GetValue(i));
propertyInfo.SetValue(model, _value, null);
} break;
case "System.Single":
{
_value = Convert.ToSingle(dr.GetValue(i));
propertyInfo.SetValue(model, _value, null);
} break;
case "System.Decimal":
{
_value = Convert.ToDecimal(dr.GetValue(i));
propertyInfo.SetValue(model, _value, null);
} break;
case "System.Double":
{
_value = Convert.ToDouble(dr.GetValue(i));
propertyInfo.SetValue(model, _value, null);
} break;
case "":
{
_value = Convert.ToDateTime(dr.GetValue(i));
propertyInfo.SetValue(model, _value, null);
} break;
default: break;
}
}
}
}
dr.Close();
conn.Close();
}
/// <summary>
/// 根据不同的数据库命令对象返回该类型数据库参数的前缀格式化字符串
/// </summary>
/// <param name="command"></param>
/// <returns></returns>
private string GetParameterFormat(DbCommand command)
{
if (!ParametersFormat.ContainsKey(command.GetType()))
{
this.Open();//读取参数前缀时需打开数据库连接
ParametersFormat.Add(
command.GetType(),
command.Connection.GetSchema("DataSourceInformation")
.Rows[0]["ParameterMarkerFormat"].ToString());
//conn.Close();在真正执行语句的时候去关闭,避免重复打开
}
return ParametersFormat[command.GetType()];
}
private void Open()
{
if (conn.State == ConnectionState.Closed)
{
conn.Open();
}
}
private void Close()
{
if (conn.State == ConnectionState.Open)
{
conn.Close();
}
}
/// <summary>
/// 替换DML语句里的参数前缀
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public string Replace(string str)
{
return str.Replace("$", retParaformat.Substring(0, 1));
}
}
}
文件下载:PeaceHelper.cs
到此这篇关于告别ADO.NET实现应用系统无缝切换的烦恼(总结篇)就介绍到这了。来者要惜,去者要放。人生是一场,不是所有人都会去同一个地方。路途的邂逅,总是,分手的驿高中生作文,总是凄凉。不管喜与愁,该走的还是要走,该来的终究会来。人生的旅程,大半是孤单。懂得,来的俱是美丽;舍得放手,走的不成负担。对过去,要放;对现在,要惜;对将来,要信。更多相关告别ADO.NET实现应用系统无缝切换的烦恼(总结篇)内容请查看相关栏目,小编编辑不易,再次感谢大家的支持!