当我试图从数据库中搜索用户控件中的数据时,它会搜索或过滤我在搜索文本框中键入的数据。这是我用来尝试搜索或过滤的代码
SqlConnection cn;
SqlCommand cm;
SqlDataReader dr;
private Label name;
private Label amount;
private Label descrip;
public Form1()
{
InitializeComponent();
cn = new SqlConnection(@"Data Source=(LocalDB)");
}
private void Form1_Load(object sender, EventArgs e)
{
GetData();
}
private void GetData()
{
cn.Open();
cm = new SqlCommand("Select * from Bills where (billname) like '%" + txtSearch.Text + "%'", cn);
dr = cm.ExecuteReader();
while (dr.Read())
{
long len = dr.GetBytes(0, 0, null, 0, 0);
byte[] array = new byte[System.Convert.ToInt32(len) + 1];
dr.GetBytes(0, 0, array, 0, System.Convert.ToInt32(len));
name = new Label();
name.Text = dr["billname"].ToString();
descrip = new Label();
descrip.Text = dr["billdescrip"].ToString();
amount = new Label();
amount.Text = dr["billamount"].ToString();
}
dr.Close();
cn.Close();
}
private void txtSearch_TextChanged(object sender, EventArgs e)
{
GetData();
}
当我在txtSearch.text框中键入一些内容时,结果返回为空,并且不会显示我试图在txtSarch.text框内搜索的内容。
"Select * from Bills where (billname) like '%" + txtSearch.Text + "%'"
在我看来,您有一个数据库表Bills
,其中有一列BillName
。操作员在TextBoxtxtSearch
中键入一些文本,并且您希望获取BillName以TextBox中的文本开头的所有Bill。
我在这里看到了几个问题
SQL注入
SQL注入是一种可能会破坏数据库的代码注入技术。SQL注入是最常见的网络黑客技术之一。SQL注入是在SQL语句中放置恶意代码
看看如果运算符键入以下文本,Sql文本会是什么:"约翰%;DROP TABLE账单--">
Select * from Bills where (billname) like %John%; DROP TABLE Bills; --%
你会失去所有的比尔!
有关SQL注入的详细信息
解决方案:永远不要将输入数据添加到sql字符串中始终将其作为参数添加!
开始使用using语句
数据库连接是一种稀缺的资源:您不应该让它的生存时间超过所需的时间。此外,如果SQL查询遇到异常,则连接和数据读取器不会关闭。
养成习惯,无论何时对象实现IDisposable,都应该使用using语句来使用它。
这样,无论发生什么,在using语句结束时,都可以确保所有内容都被正确地刷新、写入、关闭和处理。
SqlConnection、SqlCommand和SqlDataReader应该是GetData的私有成员。通过这种方式,你可以确信没有人可以篡改你的连接;您隐藏了如何从数据库(SQL和SqLCommand,或Entity Framework和LINQ?)中获取数据,从而使将来的更改更容易。代码的读者不必检查这些变量在哪里被使用,也没有人滥用它,从而使代码更容易理解。当然,这将使将GetData重新用于其他目的成为可能。
这就引出了第三个改进:
将数据与显示方式分开
在现代编程中,你会看到日期(=模型)和数据显示方式(=视图)之间越来越多的分离。
- 分离可以更好地重用代码:例如:如果您想在控制台程序、WPF程序甚至其他Form中使用模型,则可以重用模型类
- 分离隐藏了获取数据的方式和位置:它是数据库吗?它是CSV文件还是XML?你在使用实体框架吗
- 这种隐藏允许将来更改,而无需更改所有表单
- 这种隐藏还使表单变得更小、更易于理解
- 在开发表单时,您可以模拟实际数据:只需创建一个为您提供示例数据的伪类,而不必担心数据库
- 您可以对模型进行单元测试,而不需要表单
- 这几乎不是什么额外的工作
因此,您将拥有Model类:您的数据以及数据的保存方式,将再次获取;和查看类:您的窗体。您需要一个适配器类来使模型适应视图:ViewModel。这三者合在一起缩写为MVVM。考虑做一些关于MVVM的背景阅读。
落实三个建议
我们创建了一个类,可以保存账单(和其他项目:客户?订单?产品?等),以后即使重新启动程序,您也可以再次检索它们。类似于仓库、存储库,您可以在其中存储物品并再次提取。
interface IOrderRepository
{
int AddBill(Bill bill); // return Id of the Bill
Bill FindBill(int Id); // null if not found
// your GetData:
IEnumerable<Bill> FetchBillsWithNameLike(string name);
... // other methods, about Customers, Orders, etc
}
实施:
class OrderRepository : IOrderRepository
{
private string ConnectionString {get;} = @"Data Source=(LocalDB)";
private IDbConnection CreateConnection()
{
return new SqlConnection(this.ConnectionString);
}
FetchBillsWithNameLike:的实现
public IEnumerable<Bill> FetchBillsWithNameLike(string name)
{
using (IDbConnection dbConnection = this.CreateConnection())
{
const string sqlText = "Select Id, BillName, CustomerId, ..."
+ " from Bills where (billname) like %@Name%";
using (IDbCommand = dbConnection.CreateCommand())
{
// fill the command and the parameter:
dbCommand.CommandText = sqlText;
dbCommand.AddParameterWithValue("@Name", name);
// execute the command and enumerate the result
dbConnection.Open();
using (IDatareader dbReader = dbCommand.ExecuteReader())
{
while (dbReader.Read())
{
// There is a Bill to read
Bill bill = new Bill
{
Id = dbReader.ReadInt32(0),
Name = dbReader.ReadString(1),
CustomerId = dbReader.ReadInt32(2),
...
};
yield return bill;
}
}
}
}
}
// implement rest of interface
}
几个改进:
- 连接字符串是一个属性。如果您决定对所有100种方法使用不同的连接字符串:只需更改一个位置
- 你可以隐藏你得到连接字符串的地方:这里它是一个常量,但如果你决定在未来的版本中从配置文件中读取它:除了这个方法,没有人需要知道
- 如果隐藏您正在使用SqlConnection,则返回接口。如果在未来的版本中,您决定创建不同形式的IDbConnection,例如针对不同类型的数据库(如SQLite),则没有人知道您创建的是SqlLiteConnection对象而不是SqlConnection
- 类似地:隐藏SqlCommand,使用接口IDbCommand
- 数据库连接在需要之前未打开。这使得其他人可以使用数据库,只要你不使用它
- 到处都是
using
语句:如果发生任何异常,所有对象都会被正确地关闭和处理 - 我自己不创建DbCommand,我要求DbConnection为我创建它,所以我不必麻烦实际的DbConnection使用哪种类型的命令:它是SqlCommand吗?SQLiteCommand
- 如果需要,我指定表中的哪些列。如果将来添加了一些列,而我不需要它们,我将不会获取比我想要的更多的数据。类似地:如果列被重新排序,它仍然可以工作
最重要的更改:使用SQL参数来防止恶意SQL注入。
SQL文本中的参数通常由前缀
@
识别参数是使用扩展方法AddParameterWithValue`添加的。一些数据库将此作为DbCommand(例如:SQLite)中的一种方法
当读取提取的数据时,我不会读取比来电者想要的更多的账单。因此,如果他用以下代码给我打电话:并不是所有的账单都会被阅读:
IOrderDepository存储库=。。。string name=这个。ReadName();bool billsWithNameAvailable=存储库。FetchBillsWithName(name)。Any();
这里,我的来电者只想知道是否有任何同名的账单。读者根本不会创建任何账单。
因为SQL文本是Select Id, ...
,我读了dbReader.GetInt32[0]
等,所以即使在插入或重新排序表的列之后,我的代码仍然可以工作。
好的是,您将能够在不必使用Form的情况下对方法FetchBillsWithName进行单元测试:您可以测试如果根本没有数据库,或者如果没有Bills表,或者空表,或者表不包含列BillName会发生什么。或者如果输入文本为空会发生什么。你可以在不需要表单的情况下对各种错误进行单元测试
表格
class Form1 : ...
{
private IOrderRepository Repository {get;} = new OrderRepository();
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
GetData();
}
private void GetData()
{
string name = this.txtSearch.Txt;
foreach(Bill fetchedBill in this.Repository.FetchBillsWithNameLike(name))
{
this.ProcessBill(fetchedBill);
}
}
private void ProcessBill(Bill fetchedBill)
{
// do your stuff with the label;
}
}
因为我将模型与视图分离,所以视图要简单得多:更容易看到实际发生的事情:你关注的是表单,而不是如何以及在哪里获得数据。
在开发过程中,当您还没有数据库时,您可以创建一个虚拟存储库并测试您的表单:
class DummyRepository : IOrderRepository
{
private Dictionary<int, Bill> Bills {get;} = ... // fill with some sample Bills
// TODO: implement IOrderRepository, using this.Bills
}
如果以后你决定不从数据库中获取数据,而是从互联网上获取数据,那么你的表格几乎不需要更改。它仍然可以使用IOrderRepository
结论
- 通过将模型与视图分离,模型和视图都更容易理解。更易于重用、更改、维护和单元测试。两者都可以独立开发
- 过程很小,只有一个任务:这使得我们可以重用过程。更改仅在一个过程中进行
- 通过使用接口,我隐藏了数据的获取方式和位置:SQL?CSV文件?互联网
- 通过使用using语句,程序可以得到更充分的证明:在异常之后,所有内容都被属性关闭并处理
- 通过使用SQL参数,我防止了恶意使用SQL注入