"Select * from Bills where (billname) like '%" + txtSearch.Text + "%'"
在我看来,您有一个数据库表Bills,其中有一列BillName。操作员在 TextBox txtSearch 中键入一些文本,并且您想要获取所有 BillName 以 TextBox 中的文本开头的账单。
我发现这里有几个问题
SQL 注入
SQL 注入是一种可能会破坏数据库的代码注入技术。
SQL 注入是最常见的网络黑客技术之一。
SQL注入是在SQL语句中放置恶意代码
如果操作员键入以下文本,请查看您的 Sql 文本:
"John%; DROP TABLE Bills;--"
Select * from Bills where (billname) like %John%; DROP TABLE Bills; --%
你会失去所有的账单!
More information about SQL Injection
解决方案:永远不要将输入数据添加到您的 sql 字符串中!始终将其作为参数添加!
开始使用 using 语句
数据库连接是一种稀缺资源:您不应该让它保持活动的时间超过需要的时间。此外,如果您的 SQL 查询遇到异常,连接和数据读取器不会关闭。
养成习惯,每当一个对象实现 IDisposable 时,您都应该使用 using 语句来使用它。
这样,您可以放心,无论发生什么,在 using 语句结束时,所有内容都已正确刷新、写入、关闭和处置。
SqlConnection、SqlCommand 和 SqlDataReader 应该是 GetData 的私有成员。这样您就可以确定没有人可以篡改您的连接;您隐藏了从数据库中获取数据的方式(SQL 和 SqlCommand,还是实体框架和 LINQ?),从而使未来的更改更容易。您的代码的读者不必检查这些变量的使用位置,并且没有人滥用它,从而使您的代码更易于理解。当然,这也使得将 GetData 重用于其他目的成为可能。
这让我想到了第三个改进:
将数据与显示方式分开
在现代编程中,您会越来越多地看到日期(= 模型)和数据显示方式(= 视图)之间的区别。
- 分离可以更好地重用代码,例如:如果您想在控制台程序、WPF 程序甚至不同的表单中使用您的模型,您可以重用模型类。
- 分离隐藏了您获取数据的方式和位置:它是数据库吗?它是 CSV 文件还是 XML?你在使用实体框架吗
- 这种隐藏允许将来进行更改,而无需更改所有表单
- 这种隐藏还使您的表单更小更易于理解
- 在开发表单时,您可以模拟实际数据:只需创建一个为您提供示例数据的虚拟类,而无需担心数据库
- 您可以对模型进行单元测试,而无需表单
- 几乎没有任何额外的工作。
因此您将拥有模型类:您的数据,以及如何保存,再次获取;和查看课程:您的表格。您需要一个适配器类来使模型适应视图: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)
-
读取获取的数据时,我不会读取比调用者想要的更多的账单。因此,如果他使用以下代码给我打电话:并非所有账单都会被读取:
IOrderRepository 存储库 = ...
字符串名称 = this.ReadName();
bool billsWithNameAvailable = repository.FetchBillsWithName(name).Any();
在这里,我的来电者只想知道是否有任何带有该名称的账单。读者根本不会创建任何账单。
因为 SQL 文本是 Select Id, ... 并且我阅读了 dbReader.GetInt32[0] 等,我的代码仍然可以工作,即使在插入或重新排序表的列之后。
好消息是,您将能够对方法 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
}
如果以后您决定不再从数据库中获取数据,而是从 Internet 获取数据,则您的表单几乎不需要更改。它仍然可以使用IOrderRepository
结论
- 通过将模型与视图分离,模型和视图都更容易标记和理解。更容易重用、更改、维护和单元测试。两者都可以独立开发
- 过程很小,只有一个任务:这使得我们可以重用过程。仅在一个过程中进行更改
- 通过使用接口,我隐藏了获取数据的方式和位置:SQL? CSV 文件?互联网?
- 通过使用 using 语句,该程序得到了更充分的证明:出现异常后,所有内容都被属性关闭和处置
- 通过使用 SQL 参数,我防止了恶意使用 SQL 注入。