WinForm遍历数据表及外键关联填充实体数据失效问题
问题背景
我创建了两张数据表:Members 和 Contracts。
Contracts表设置外键,关联Members表的ID字段。- 项目中定义了两个实体类:
Member和Contract。 - 项目主静态类
Club维护了一个存储Contract对象的ArrayList集合,每个Contract实例都包含一个Member类型的字段。
当前需要在Club类中实现逻辑:将数据库中所有Contract记录关联匹配正确的Member字段值后,填充到前述ArrayList集合中。
但编写的Club类Populate方法运行异常,无法正常工作,故障代码如下:
public static void Populate() { string connStr = @"Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=Contribution;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"; SqlConnection con = new SqlConnection(connStr); SqlCommand cmd = new SqlCommand("SELECT * FROM Contracts", con); SqlDataReader dr; try { con.Open(); dr = cmd.ExecuteReader(); while(dr.Read()) { Member newMember = null; int memberID = dr.GetInt32(dr.GetOrdinal("memberID")); MessageBox.Show(memberID.ToString()); SqlCommand cmd2 = new SqlCommand("SELECT * FROM Members WHERE ID = @memberID", con); cmd2.Parameters.AddWithValue("@memberID", memberID); cmd2.ExecuteNonQuery(); SqlDataReader dr2; dr2 = cmd2.ExecuteReader(); while (dr2.Read()) { string name = dr2.GetString(dr2.GetOrdinal("name")); DateTime birthDate = DateTime.ParseExact(dr2.GetString(dr2.GetOrdinal("birthDate")), "dd/MM/yyyy", null); DateTime joinDate = DateTime.ParseExact(dr.GetString(dr.GetOrdinal("joinDate")), "dd/MM/yyyy", null); bool isPlaying = (dr2.GetInt32(dr2.GetOrdinal("isPlaying")) == 1 ? true : false); newMember = new Member(name, birthDate, joinDate, isPlaying); MessageBox.Show(name); } dr2.Close(); Club.AddContract(new Contract(newMember)); } dr.Close(); } catch (Exception ex) { MessageBox.Show(ex.Message); } finally { con.Close(); } }
故障现象
测试运行未得到预期结果:
- 运行后无有效数据返回
- 实现的计算总缴费额、获取最年轻会员等方法,相关字段取值均为空或null
- 已做调整:改用参数化查询、修复部分已知小问题,但故障仍然存在。
故障原因与修复方案
代码有3个直接导致数据读取失败的硬错误,按优先级修复即可:
- 连接被占用导致第二个查询无法执行
在第一个SqlDataReader(变量dr)未关闭的情况下,复用同一个SqlConnection执行第二个查询,而SQL Server默认关闭MARS(多活动结果集)功能,这种操作会直接抛出连接占用异常,这是拿不到Member数据的核心原因。
两种修复方案选其一即可:
- 推荐方案:先一次性把所有Contracts关联的memberID读进内存集合,关闭第一个reader之后,再遍历集合逐个查询关联的Member信息,没有额外配置成本,也不容易出隐式bug
- 备选方案:在连接字符串中添加
MultipleActiveResultSets=True开启MARS功能,不推荐新手使用,容易出现未释放的reader长期占用连接的问题
- 读取joinDate字段用错了DataReader实例
代码中读取joinDate时写的是dr.GetOrdinal("joinDate"),用的是外层读取Contracts表的dr对象,不是读取Members表的dr2对象,这里会直接抛出“列不存在”的异常,或者读取到错误的值,改成dr2.GetOrdinal("joinDate")即可。 - 存在多余的无效查询调用
在执行cmd2的成员查询之前,多写了一行cmd2.ExecuteNonQuery(),这个方法仅用于执行增/删/改操作,执行SELECT语句时调用这个方法不会返回结果集,平白多执行一次无效查询,直接删除这行即可。
修复后可运行参考代码
public static void Populate() { string connStr = @"Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=Contribution;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"; List<int> contractMemberIds = new List<int>(); // using块自动释放连接、命令、读取器资源,不用手动写Close,避免资源泄漏 using (SqlConnection con = new SqlConnection(connStr)) { con.Open(); // 第一步:先读取所有合同关联的会员ID SqlCommand cmd = new SqlCommand("SELECT memberID FROM Contracts", con); using (SqlDataReader dr = cmd.ExecuteReader()) { while (dr.Read()) { contractMemberIds.Add(dr.GetInt32(dr.GetOrdinal("memberID"))); } } // 第一个reader在这里自动释放,连接不再被占用 // 第二步:逐个查询会员信息,生成Contract对象 foreach (int memberID in contractMemberIds) { Member newMember = null; SqlCommand cmd2 = new SqlCommand("SELECT * FROM Members WHERE ID = @memberID", con); cmd2.Parameters.AddWithValue("@memberID", memberID); using (SqlDataReader dr2 = cmd2.ExecuteReader()) { while (dr2.Read()) { string name = dr2.GetString(dr2.GetOrdinal("name")); DateTime birthDate = DateTime.ParseExact(dr2.GetString(dr2.GetOrdinal("birthDate")), "dd/MM/yyyy", null); // 修复:使用dr2读取joinDate字段 DateTime joinDate = DateTime.ParseExact(dr2.GetString(dr2.GetOrdinal("joinDate")), "dd/MM/yyyy", null); bool isPlaying = dr2.GetInt32(dr2.GetOrdinal("isPlaying")) == 1; newMember = new Member(name, birthDate, joinDate, isPlaying); } } Club.AddContract(new Contract(newMember)); } } }
额外优化建议
- 如果数据量不大,直接写联表SQL一次读完全部数据即可,不用循环查数据库,性能更高也不会出连接占用问题:
SELECT m.name, m.birthDate, m.joinDate, m.isPlaying FROM Contracts c LEFT JOIN Members m ON c.memberID = m.ID - 如果Members表中
birthDate、joinDate字段本身就是SQL Server的datetime/date类型,不要用GetString再做日期解析,直接调用dr2.GetDateTime(列索引)读取即可,能避免日期格式不匹配导致的解析报错。 - 不推荐用
ArrayList存储对象,建议改用泛型集合List<Contract>,可以避免装箱拆箱的性能损耗,也不用做强制类型转换。
内容的提问来源于stack exchange,提问作者user14579575
相关产品推荐
相关产品推荐

