ado.net笔记
- ADO.Net基础
- 程序要和数据库交互要通过ADO.Net进行,通过ADO.Net就能在程序中执行SQL语句了。ADO.Net中提供了对各种不同数据库的统一操作接口。
- 直接在项目中内嵌mdf文件的方式使用SQLServer数据库(基于服务的数据库)。mdf文件随着项目走,用起来方便,和在数据库服务器上创建数据库没有什么区别,运行的时候会自动附加(Attach)。
- 双击mdf文件会在“服务器资源管理器”中打开,管理方式和在Management Studio没有什么本质不同。要拷贝mdf文件需要关闭所有指向mdf文件的连接。
- 正式生产运行的时候附加到SQLServer上、修改连接字符串即可,除此之外没有任何的区别,在“数据库”节点上点右键“附加”:在数据库点上→任务→分离就可以得到可以拷来的拷去的mdf文件。
- 用的时候要在控制台、WinForm项目中在Main函数最开始的位置加入备注的代码。ASP.Net项目中不需要。
- 连接SQLServer
- 连接字符串:程序通过连接字符串指定要连哪台服务器上的、哪个实例的哪个数据库、用什么用户名密码等。
- 项目内嵌mdf文件形式的连接字符串
“DataSource =.\SQLEXPRESS:AttachDBFilename=|DataDirectory|\Database1.mdf;Integrated Security=True; User Instance=True”。其中”.\SQLEXPRESS”表示本机上的SQLEXPRESS实例,如果数据库实例名不是SQLEXPRESS,则需要修改。”Database1.mdf”为mdf的文件名。
//使用using,那么conn只能在using的{}内使用,之外就会释放。当然也可以使用try catch语句。
using(
SqlConnection conn = new SqlConnection(
@"Data Source=.\SQLEXPRESS;
AttachDBFilename = |DataDirectory|\Database1.mdf;
Integrated Security = True;
User Instance = True"
)
){
conn.Open();
}
必有的代码:
//加上下面的代码,可以去看数据库中数据的更新情况,否则,即使更新了数据库也是不显示的。
string dataDir = AppDomain.CurrentDomain.BaseDirectory;
if(dataDir.EndsWith(@"\bin\Debug\") ||
dataDir.EndsWith(@"\bin\Release\"))
{
dataDir = System.IO.Directory.GetParent(dataDir).Parent.Parent.FullName;
AppDomain.CurrentDomain.SetData("DataDirectory",dataDir);
}
- ADO.Net中通过SqlConnection类创建到SQLServer的连接,SqlConnection代表一个数据库连接,ADO.Net中的连接等资源都实现了Idisposable接口,可以使用using进行资源管理,执行备注中的代码如果成功了就ok。
- 可能出现的问题:连接不上数据库:连接字符串有错误。
- 执行简单的Insert语句
- SqlCommand表示向服务器提交一个命令(SQL语句等),CommandText属性为要执行的SQL语句,ExecuteNonQuery方法执行一个非查询语句(Update、Insert、Delete等)
using(SqlCommand cmd = conn.CreateCommand())
{
cmd.CommandText = “Insert into T_Users(UserName,Password) values (‘admin’,’888888’)”;
cmd.ExecuteNonQuery();
} - ExecuteNonQuery返回值是执行的影响行数。
- 注意:
string username = ‘test’;
……
cmd.CommandText = “Insert into T_Users(UserName,Password) values(username,’888888’)”;
- ExecuteScalar
- SqlCommand的ExecuteScalar方法用于执行查询,并返回查询所返回的结果集中的第一行的第一列,因为不能确定返回值的类型,所以返回值是object类型。
l cmd.CommandText = “select count(*) from T_Users”;
l int i = Convert.ToInt32(cmd.ExecuteScala());
l cmd.CommandText = “select getdate()”;
l DateTime dt = Convert.ToDataTime(cmd.ExecuteScalar());
- 得到自动增长字段的主键值,在values关键词前加上output inserted.Id,其中Id为主键字段名。执行结果就是插入的主键值,用ExecuteScalar执行最方便。
l cmd.CommandText = “Insert into T_Users(UserName,Password) output inserted.Id values(‘admin’,’888888’);
l int i = Convert.ToInt32(cmd.ExecuteScalar());
- 执行查询
- 执行有多行结果集的用ExecuteReader
l SqlDataReader reader = cmd.ExecuteReader();
l while(reader.Read())
l {
Console.WriteLine(reader.GetString(1));
l }
- reader的GetString、GetInt32等方法只接受整数参数,也就是序号,用GetOrdinal方法根据列名动态得到序号。
- using与close与dispose。如果没有用using,只是close那么还可以open,而dispose就是直接销毁了,不能在open。使用using,当变量出了using区域,就会看有没有close,如果没有,先执行close,再执行dispose。使用using比较好,如果使用别的,可能不会执行到dispose语句。造成别的时候连接不上了。
- 另一种写法:
reader.GetString(reader.GetOrdinal("UserId")); == reader.GetString(0)
reader.GetString(reader.GetOrdinal("UserName")); == reader.GetString(1)
reader.GetString(reader.GetOrdinal("Password")); == reader.GetString(2)
- SQL注入漏洞攻击
- 登录判断:cmd.CommandText = "select count(*) from Users where UserName = '"+ userName + "' and Password = '" + password +"'"; 通过将参数拼到SQL语句中。
- 构造恶意的Password: 12’ or ‘1’ = ‘1 这样的话,就会成功登录了。
- 防范注入漏洞攻击的方法:不使用SQL语句拼接,通过参数赋值。
- 查询参数
- SQL语句使用@UserName表示“此处用参数代替”,向SqlCommand的Parameters中添加参数cmd.CommandText = "select count(*) from Users where UserName = @UN and Password = @P";
cmd.Parameters.Add(new SqlParameter("UN", userName));
cmd.Parameters.Add(new SqlParameter("P", password));
- 参数在SQLServer内部不是简单的字符串替换。SQLServer直接用添加的值进行比较,因此不会有注入漏洞攻击。
- 案例
- 用户界面中进行用户名的增加。输错三次禁止登录,用数据库记录ErrorTimes。
l 注意:在同一个连接中,如果SqlDataReader没有关闭,那么不能执行update类型的语句。因为指针还在指着它,是不能被别人访问的。可以再写一个函数,把它放在函数中。
- 数据导入:从文本文件导入用户信息。易错点:Parameter的重复添加。
l 连接数据库用创建SQL命令,执行一次就可以了。不要写在循环里面。
l 有关乱码问题:使用nvarchar而不是varchar,插入的汉字之前加N
- 数据导出:将用户信息导出到文本文件。
- 问题:在导入汉字的过程中,如何保存使之在数据库中存入的不是乱码?
设置读入时候的字符编码为Default即可。默认就是UTF-8的。
- 读取配置中的字符串
- 将字符串写在代码中缺点:多次重复。
l 添加App.config文件。App.config文件是.Net的通用配置文件,在ASP.Net中也能同样使用。
l 在App.config中添加connectionString段,添加一个add项,用name属性起一个名字(比如DbConnStr),connectionString属性指定连接字符串。
l 在“引用”节点上点右键“添加引用”,找到System.configuration。不是所有.Net中的类都能直接调用,类所在的Assembly要被添加到项目的引用中才可以。
l ConfigurationManager.ConnectionStrings[“DbConnStr”].ConnectionString得到连接字符串。
l 如何改变了位置,只需在App.config文件中将连接字符串改变即可。
- DataSet
- SqlDataReader是连接相关的,SqlDataReader中的查询结果并不是放到程序中的,而是放在数据库服务器中,SqlDataReader只是相当于放了一个指针(游标),只能读取当前游标指向的行,一旦连接断开就不能再读取,这样做的好处就是无论查询结果有多少条,对程序占用的内存几乎没有影响。
- SqlDataReader对于小数据量的数据来说带来的只能麻烦,优点可以忽略不计。ADO.Net中提供了数据集的机制,将查询结果填充到本地内存中,这样连接断开、服务器断开都不影响数据的读取。
- DataSet dataset = new DataSet();
- SqlDataAdpater adpater = new SqlDataAdpater(cmd);
- Adpter.Fill(dataset);
- SqlDataAdpater是DataSet和数据库之间沟通的桥梁。数据集DataSet包含若干表DataTable,DataTable包含若干行DataRow,foreach(DataRow row in dataset.Tables[0].Rows) row[“Name”]
- DataSet的更新
- 可以更新行row[“Name”]=”changed”、删除行datatable.Rows.Remove()、新增行datatable.NewRow()。这一切都是修改的内存中的DataSet,没有修改数据库中的。
- 可以调用SqlDataAdapter的Update方法将对DataSet的修改提交到数据库,Update方法有很多重载方法,可以提交整个DataSet、DataTable或者若干DataRow。但是需要为SqlDataAdapter提供DeleteAdpater提供DeleteCommand、UpdateCommand、InsertCommand它才知道如何将对DataSet的修改提交到数据库,由于这几个Command要求的格式非常的苛刻,因此开发人员自己写非常的困难,可以用SqlCommandBuilder自动生成这几个Command,用法简单,new SqlCommandBuiler(adapter)。查看生成的Command。SqlCommandBuilder要求表必须有主键。
- 通过DataRow的RowState可以获得行的状态(删除、修改、新增等);
- 调用DataSet的GetChangeds()方法得到变化的结果集,降低传递的资源占用。
- 可空类型
- Int? 可空的int,触决数据库和对int不能为空的不同设置的问题
- Int? i3 = 4; int i4 = i3.Value;
- 弱类型DataSet的缺点
- 只能通过列名引用,dataset.Tables[0].Rows[0][“Age”],如果写错了列名编译时不会发现错误,因此开发时必须要记着列名。
- Int age = Convert.ToInt32(dataset.Rows[0][“Age”]),取到的字段的值是object类型,必须小心翼翼的进行类型转换。
- 将DataSet传递给其他使用者,使用者很难识别出有哪些列可以使用。
- 运行时才能知道所有列名,数据绑定麻烦,无法使用Winform、ASP.Net的快速开发功能。
- 自己动手写强类型DataSet(类型化DataSet,TypeDataSet),创建继承自DataSet的PersonDataSet类,封装出int?Age等属性和bool IsAgeNull等方法,向PersonDataSet中填充。
- VS自动生成强类型DataSet
- 添加→新建项→数据集
- 将表从服务器资源管理器拖放到DataSet中。注意拖放过程是自动根据表结构生成强类型DataSet等类,没有把数据也拖过来,程序还是连的那个数据库,自动将数据库连接字符串写在了App.Config中。
- 代码中使用DataSet示例:CC_RecordTableAdapter adapter = new CC_RecordTAbleAdapter(); 如何得知Adapter为类名?选中DataSet中下半部分的Adapter,Name属性就是类名。需要右键点击类名→解析
- 取得所有数据:adapter.GetData(),例子程序:遍历显示所有数据,i<adapter.GetData().Count; adapter.GetData()[i].Age。
- 常见问题:类名敲不对,表名+TableAdapter,表名+DataTable,表名+Row,然后用“解析”来填充类名。
- 常见问题:类的内部定义的类要通过包含namespace的全名来引用,不能省略。类的内部定义的类就能避免同一个namespace下类不能重名的问题。
- 更新DataSet
- 调用Adapter的Update方法就可以将DataSet的改变保存到数据库。Adapter.Update(datatable)
- 要调用Update方法更新必须设置数据库主键,后面的Delete也是如此。
- 常见错误:“当传递具有已修改行的DataRow集合时,更新要求有效的UpdateCommand”,要为表设置主键。“谁都变了,唯有主键不会变”,程序要通过主键来定位要更新的行,如果没有设定,可以先到数据库中设置主键,然后在DataSet的对应DataTable上点击右键,选择“配置”,在【对话框】中点击完成。
- 其它问题
- 插入新行,调用insert方法。
- 增加字段怎么办?DataSet设计器中点【配置】,对话框中点【查询生成器】,勾选新增加的字段即可,删除字段同样如此,也可以直接在上面进行修改。
- 要修改字段就要重新配置生成,这就是强类型DataSet的弱点,因此强类型转换并不一定真的强,还是叫“类型化DataSet”吧。
- 常见错误:报错:数据为空,判断列的值为空的方法:Is ** Null
- 为什么Select方法会填充、Update方法会更新,Insert方法会插入?看看Adapter的SelectCommand等属性,是那些SQL语句在起作用,如果在需要完全可以自己手工调整。
- 增加新的SQL语句
- 设计器的Adapter中点右键,选择“添加查询”→“使用SQL语句”,就可以添加多种类型的SQL语句,如果是“Select(返回行)”则SQL语句的列必须是对应DataSet类的父集合,生成两个方法:FillBy*和GetBy*,方法名自己要重新命名,Fillby是将结果填充到现有的DataSet,GetBy是将结果以DataSet方式返回,建议两个都生成,方便以后使用,
- “Select(返回单个值)”就是ExecuteScalar
- 对于增加的SQL语句在代码中是以方法的形式使用的。方法的参数类型、顺序就是VS猜测的,如果不正确或者需要调整只要选中对应的语句,然后在【属性】窗口中修改Parameters属性即可。
- 增加新的SQL语句本质论:探寻源码:不能并发调用。
- 像使用普通类的方法一样使用Adapter。SQL语句不用再写在界面代码中。这就是一种数据访问层(DAL:Data Access Layer)
- 批量插入反应太慢(通过秒表来计时)时,就在执行语句之前,先把连接打开。
Stopwatch sw = new Stopwatch();
sw.Start();
T_UsersTableAdapter adapter = new T_UsersTableAdapter();
adapter.Connection.Open();
for (int i = 0; i < 2000; i++)
{
adapter.Insert(i.ToString(),null,i.ToString(),null);
}
adapter.Connection.Close();
sw.Stop();
MessageBox.Show(sw.Elapsed.ToString());
- 示例:向一个表中插入大量随机数据。
System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
//为Test数据库中的T_Product表与T_Manufacturer表生成数据
string connectionString = "Data Source=.;Initial Catalog=Test;Integrated Security = true";
SqlConnection conn = new SqlConnection(connectionString);
conn.Open();
SqlCommand cmd = conn.CreateCommand();
cmd.Parameters.Add("name", SqlDbType.NVarChar);
cmd.Parameters.Add("address", SqlDbType.NVarChar);
cmd.Parameters.Add("email", SqlDbType.NVarChar);
cmd.Parameters.Add("tel", SqlDbType.VarChar);
cmd.Parameters.Add("model", SqlDbType.NVarChar);
cmd.Parameters.Add("builddate", SqlDbType.Date);
double num = 0;
for (int i = 0; i < 1000; i++)
{
int randomNum = new Random().Next(Int32.MaxValue);
DateTime builddate = new DateTime(1998, 1, 1).AddDays(new Random().Next(10000));
AddDataToManufacturer(cmd, "深圳恒丰塑化材料有限公司" + randomNum, "广东深圳市光明新区" + randomNum, "guangdong" + randomNum + "@163.com", "12345678902" + randomNum, "经销批发" + randomNum, builddate);
Console.WriteLine(string.Format("第{0}条完成......", num+1));
num++;
AddDataToManufacturer(cmd, "北京伊特伟业科技公司" + randomNum, "北京市朝阳区" + randomNum, "yite" + randomNum + "@163.com", "12345678903" + randomNum, "个体经营" + randomNum, builddate);
Console.WriteLine(string.Format("第{0}条完成......", num + 1));
num++;
AddDataToManufacturer(cmd, "苍南县飞翔彩印厂" + randomNum, "浙江苍南县" + randomNum, "cangnian" + randomNum + "@163.com", "12345678901" + randomNum, "生产厂家" + randomNum, builddate);
Console.WriteLine(string.Format("第{0}条完成......", num + 1));
num++;
}
Console.WriteLine("\r\n完成");
Console.WriteLine(string.Format("共添加{0}条记录", num));
stopwatch.Stop();
Console.WriteLine(string.Format("共用{0}时{1}分{2}秒{3}毫秒", stopwatch.Elapsed.Hours,stopwatch.Elapsed.Minutes,stopwatch.Elapsed.Seconds,stopwatch.Elapsed.Milliseconds));
Console.ReadKey();
}
/*
* 共添加3000条记录
* 共用0时0分16秒401毫秒
* 共用0时0分17秒726毫秒
* 共用0时0分16秒960毫秒
*/
public static void AddDataToManufacturer(SqlCommand cmd,string name, string address,string email,string tel,string model,DateTime builddate)
{
cmd.CommandText = @"insert into T_Manufacturer(ManufacturerName,ManufacturerAddress,ManufacturerEmail,ManufacturerTel,BusinessModel,BuildDate) values(@name,@address,@email,@tel,@model,@builddate)";
cmd.Parameters["name"].Value = name;
cmd.Parameters["address"].Value = address;
cmd.Parameters["email"].Value = email;
cmd.Parameters["tel"].Value = tel;
cmd.Parameters["model"].Value = model;
cmd.Parameters["builddate"].Value = builddate;
cmd.ExecuteNonQuery();
}