EF 数据库优先(DBFirst)
一、项目概述
本案例基于 EF DBFirst(数据库优先) + WinForm 实现: 1. 数据表 UserInfo 基础增、删、改、查 2. EF 实体状态控制 3. 数据库外键 ------ 导航属性联表查询 4. 界面 DataGridView 数据绑定与刷新
DBFirst 核心流程:先有数据库、数据表、外键关系 → VS 根据数据库自动生成实体类 + EF上下文 → 代码操作实体即可操作数据库。
二、全局核心对象
1. EF 数据库上下文
// EF核心上下文:所有数据库操作的唯一入口
BankSystemEntities context = new BankSystemEntities();
作用 : 1. 映射数据库所有表实体 2. 托管实体对象状态(新增/修改/删除/未变更) 3. 统一提交保存到数据库 SaveChanges()
三、封装方法:数据绑定刷新
// 封装:刷新表格数据
public void BindView()
{
// ToList():执行EF查询,把数据加载到内存并绑定控件
dataGridView1.DataSource = context.UserInfo.ToList();
}
知识点 : EF 查询默认延迟加载,只有调用 ToList() / Find() / Single()才会真正执行 SQL 查询数据库。
四、窗体加载事件
private void Form1_Load(object sender, EventArgs e)
{
// 窗体打开自动加载所有用户数据
BindView();
// 隐藏导航属性自动生成的关联列(界面不需要展示)
if (dataGridView1.Columns["AddressInfo"] != null)
{
dataGridView1.Columns["AddressInfo"].Visible = false;
}
}
五、功能模块拆分(核心考点)
模块1:新增数据(Button1)
private void button1_Click(object sender, EventArgs e)
{
// 1. 新建实体对象(对应数据库一行数据)
UserInfo u1 = new UserInfo()
{
Account = "123",
Password = "123",
CreateUserId = 1,
LastUpdateTime = DateTime.Now,
CreateTime = DateTime.Now,
Status = 1
};
// 2. 添加到EF上下文【仅存入内存,未写入数据库】
context.UserInfo.Add(u1);
// 3. 提交事务:同步内存所有更改到数据库
int row = context.SaveChanges();
// 4. 判断结果
if (row > 0)
{
MessageBox.Show("添加成功");
BindView(); // 刷新界面
}
else
{
MessageBox.Show("添加失败");
}
}
新增核心原理(必背) : 1. Add():仅标记为新增状态,保存到 EF 内存上下文 2. SaveChanges():真正生成 Insert 语句、提交数据库 3. 返回值:受影响的数据行数
模块2:删除数据(Button2)------两种删除方式
private void button2_Click(object sender, EventArgs e)
{
// 判断是否选中表格行
if (dataGridView1.SelectedRows.Count > 0)
{
// 1. 获取选中行主键ID
int id = Convert.ToInt32(dataGridView1.SelectedRows[0].Cells["UserId"].Value);
// 2. 根据主键查询被EF托管的实体对象
UserInfo u1 = context.UserInfo.Find(id);
// ===== EF两种删除方式(考试重点)=====
// 方式一:直接移除(简洁常用)
// context.UserInfo.Remove(u1);
// 方式二:手动设置实体状态为删除(底层原理)
context.Entry(u1).State = System.Data.Entity.EntityState.Deleted;
// 3. 提交删除到数据库
int row = context.SaveChanges();
if (row > 0)
{
MessageBox.Show("删除成功");
BindView();
}
else
{
MessageBox.Show("删除失败");
}
}
}
删除考点 : 1. 必须是被EF上下文托管的对象才能删除(Find查询出来的对象) 2. 两种删除效果完全一致 3. 不写 SaveChanges 不会真正删除数据库数据
模块3:修改数据(Button3)
private void button3_Click(object sender, EventArgs e)
{
if (dataGridView1.SelectedRows.Count > 0)
{
// 1. 获取主键
int id = Convert.ToInt32(dataGridView1.SelectedRows[0].Cells["UserId"].Value);
// 2. 查询出托管实体
UserInfo u1 = context.UserInfo.Single(m => m.UserId == id);
// 3. 修改字段值
u1.Password = "888888";
u1.Account = "787777";
// 4. 手动标记实体为【修改状态】
context.Entry(u1).State = System.Data.Entity.EntityState.Modified;
// 5. 提交更新
if (context.SaveChanges() > 0)
{
BindView();
}
}
}
修改核心原理 : 1. 先查后改(必须操作EF托管对象) 2. Modified:标记实体发生变更 3. SaveChanges 生成 Update 语句更新数据库
模块4:导航属性(超级重点)
private void button4_Click(object sender, EventArgs e)
{
// 1. 查询主表用户数据
UserInfo u1 = context.UserInfo.Find(1);
// 2. 导航属性直接获取外键关联的从表数据(无需手写联表SQL)
foreach (var a in u1.AddressInfo.ToList())
{
MessageBox.Show(a.City + "");
}
}
六、导航属性【简答题必背】
1. 定义
导航属性是实体类中用于描述两张数据表外键关联关系 的属性。 数据库通过外键关联,EF 通过导航属性关联,无需手写 JOIN 联表 SQL,直接点语法获取关联数据。
2. 本案例关系
UserInfo(用户表) 一对多 AddressInfo(地址表) 一个用户可以拥有多个地址
3. 核心优势
-
无需手写复杂联表SQL
-
面向对象方式获取关联数据
-
DBFirst 根据数据库外键自动生成
七、EF 五大实体状态(考试必背)
-
Unchanged:未变更(查询出来的默认状态)
-
Added:新增状态(Add之后)
-
Modified:修改状态(字段更新)
-
Deleted:删除状态
-
Detached:脱离上下文,不被EF托管
八、全套易错点总结
-
Add / Remove / 修改属性 只会修改内存上下文 ,必须
SaveChanges()才会更新数据库 -
删除、修改必须操作 EF托管对象(Find/Single 查询出来的)
-
导航属性依赖数据库外键关系,没有外键无法自动生成
-
DataGridView 操作前必须判断
SelectedRows.Count>0,防止空指针报错 -
DBFirst 所有实体和上下文都是数据库反向生成,禁止手动修改实体关联
九、整体执行流程总结
打开窗体 → 加载数据绑定表格 → 新增/删除/修改操作改变实体状态 → SaveChanges 提交数据库 → 重新绑定刷新界面 → 导航属性联表查询关联数据
EF数据库优先 DBFirst 执行原生SQL(增删改查)
一、知识点前置(核心考点)
1. EF两种开发模式对比
常规EF写法:使用实体对象、Add/Remove、实体状态修改,面向对象操作,不写SQL。
本案例写法:EF中执行原生SQL,适合复杂查询、存储过程、特殊SQL语句场景。
2. EF执行SQL的两个核心方法(必考)
-
SqlQuery<T>(sql,参数) :执行 查询SQL(select),返回实体集合/单个实体
-
ExecuteSqlCommand(sql,参数) :执行增删改SQL(insert/update/delete),返回受影响行数 int
3. 防SQL注入核心
全程使用 SqlParameter 参数化查询,拼接字符串极易注入漏洞,考试、项目均禁止拼接SQL。
二、全局定义与初始化
// 全局EF数据库上下文(DBFirst自动生成)
BankSystemEntities context = new BankSystemEntities();
所有原生SQL执行,都依赖EF上下文对象调用 Database 静态方法。
三、封装方法:原生SQL查询绑定表格
功能:查询全部用户数据,绑定DataGridView
public void BindView()
{
// 1. 手写原生查询SQL
string sql = "select * from UserInfo";
// 2. SqlQuery<实体>:执行查询,自动映射为UserInfo实体集合
List<UserInfo> list = context.Database.SqlQuery<UserInfo>(sql).ToList();
// 3. 绑定控件
dataGridView1.DataSource = list;
}
方法核心解析
-
context.Database.SqlQuery<T>:EF执行查询SQL专属方法 -
自动将数据库查询结果,映射为对应实体类对象
-
.ToList():延迟加载执行,真正访问数据库
四、窗体加载事件
private void Form1_Load(object sender, EventArgs e)
{
// 窗体加载自动刷新数据
BindView();
// 隐藏导航属性多余列
if (dataGridView1.Columns["AddressInfo"] != null)
{
dataGridView1.Columns["AddressInfo"].Visible = false;
}
}
五、新增功能(原生SQL Insert)
按钮1:参数化新增用户数据
private void button1_Click(object sender, EventArgs e)
{
// 1. 编写Insert语句,使用占位符 @参数名
string sql = "insert into UserInfo(Account,Password,CreateUserId,CreateTime,LastUpdateTime,Status) " +
"VALUES(@Account,@Password,@CreateUserId,@CreateTime,@LastUpdateTime,@Status)";
// 2. 定义参数数组(防SQL注入)
SqlParameter[] paras = new SqlParameter[]
{
new SqlParameter("@Account","123456"),
new SqlParameter("@Password","123456"),
new SqlParameter("@CreateUserId",1),
new SqlParameter("@CreateTime",DateTime.Now),
new SqlParameter("@LastUpdateTime",DateTime.Now),
new SqlParameter("@Status",SqlDbType.Int){ Value = 0}
};
// 3. 执行增删改SQL,返回受影响行数
int row = context.Database.ExecuteSqlCommand(sql, paras);
// 4. 判断执行结果
if (row>0)
{
MessageBox.Show("插入成功");
BindView(); // 刷新表格
}
else
{
MessageBox.Show("插入失败");
}
}
新增核心考点
-
增删改必须使用 ExecuteSqlCommand
-
禁止字符串拼接SQL,必须 SqlParameter 参数化
-
返回值 int:代表数据库受影响行数,大于0即成功
六、修改功能(原生SQL Update)
按钮2:根据ID更新指定用户数据
private void button2_Click(object sender, EventArgs e)
{
// 1. 更新SQL语句,参数占位
string sql = "update UserInfo set Account = @Account,Password =@Password where UserId =@UserId";
// 2. 封装参数
SqlParameter[] paras = new SqlParameter[]
{
new SqlParameter("@Account","123456"),
new SqlParameter("@Password","123456888"),
new SqlParameter("@UserId",5006),
};
// 3. 执行更新
int row = context.Database.ExecuteSqlCommand(sql, paras);
if (row > 0)
{
MessageBox.Show("更新成功");
BindView();
}
else
{
MessageBox.Show("更新失败");
}
}
七、删除功能(原生SQL Delete)
按钮3:根据ID删除单条数据
private void button3_Click(object sender, EventArgs e)
{
// 1. 删除SQL
string sql = "delete from UserInfo where UserId = @UserId";
// 2. 参数封装
SqlParameter[] paras = new SqlParameter[]
{
new SqlParameter("@UserId",5005),
};
// 3. 执行删除
int row = context.Database.ExecuteSqlCommand(sql, paras);
if (row > 0)
{
MessageBox.Show("删除成功");
BindView();
}
else
{
MessageBox.Show("删除失败");
}
}
八、联表查询功能(两次原生SQL查询替代导航属性)
按钮4:手动SQL联查,不依赖EF导航属性,纯SQL实现多表关联
private void button4_Click(object sender, EventArgs e)
{
// 第一步:根据ID查询主表 UserInfo 用户数据
string sql = "select * from UserInfo where UserId = @UserId";
SqlParameter paras = new SqlParameter("@UserId",SqlDbType.Int);
paras.Value = 1;
// 查询单个实体,FirstOrDefault防止空报错
UserInfo info = context.Database.SqlQuery<UserInfo>(sql, paras).FirstOrDefault();
// 第二步:根据用户ID,查询从表 AddressInfo 关联地址数据
string sql1 = "select * from AddressInfo where CreateUserId = @a";
SqlParameter paras1 = new SqlParameter("@a", SqlDbType.Int);
paras1.Value = info.UserId;
List<AddressInfo> list = context.Database.SqlQuery<AddressInfo>(sql1, paras1).ToList();
// 遍历展示关联数据
foreach (var item in list)
{
MessageBox.Show(item.City);
}
}
核心解析
-
传统EF:靠导航属性联表
-
本案例:手写两次SQL + 参数查询 实现多表关联
-
适用于:导航属性失效、复杂联表、自定义查询场景
九、终极考点总结(考试必背)
1. 两个SQL方法区别(选择题必考)
-
SqlQuery<T>:执行查询语句(select),返回实体集合
-
ExecuteSqlCommand:执行增删改语句(insert/update/delete),返回受影响行数
2. 参数化查询作用
-
防止SQL注入漏洞
-
自动匹配数据类型,避免类型报错
-
提升SQL执行效率,支持语句缓存
3. 两种EF开发方式区别
-
对象方式:Add、Remove、状态修改,无需写SQL,简单CRUD首选
-
原生SQL方式:手写SQL语句,适合复杂查询、批量操作、特殊业务
4. 易错点
-
查询用 ExecuteSqlCommand 会报错,增删改用 SqlQuery 无返回值
-
拼接SQL字符串会导致 SQL注入漏洞,考试直接扣分
-
SqlQuery 查询结果必须 ToList() / FirstOrDefault() 才会执行访问数据库
EF CodeFirst 代码优先
一、CodeFirst 核心概念(简答题必背)
代码优先(CodeFirst) : 1. 先写 C# 实体类、通过特性标签定义字段约束 2. 通过 EF 迁移命令,自动根据实体类反向生成数据库、数据表、字段类型、约束 3. 完全不用手动建表,全部代码控制数据库结构
对比 DBFirst: DBFirst:先有数据库 → 反向生成实体 CodeFirst:先写实体代码 → 自动生成数据库
二、CodeFirst 搭建完整五步流程(必考大题)
- 安装 EF 框架 NuGet 包 2. 编写实体类 + 数据特性标签(约束、类型、主键、长度) 3. 编写 EF 数据库上下文类(继承 DbContext) 4. 配置连接字符串 5. 执行 EF 迁移命令,自动生成/更新数据库
三、实体类特性标签大全(考试超级重点)
存放于命名空间: using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema;
UserInfo 实体类完整拆解
// Table 特性:指定数据库生成的表名
[Table("UserInfo")]
internal class UserInfo
{
// 主键 + 自增
[Key] // 设置该字段为主键
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] // 数据库自增
public int Id { get; set; }
// 账号字段完整约束
[Required] // 非空约束(不能为空)
[MaxLength(20)] // 最大长度20
[MinLength(6)] // 最小长度6
[Column(TypeName = "varchar")] // 指定数据库字段类型
public string Account { get; set; }
// 密码字段完整约束
[Required]
[MaxLength(20)]
[MinLength(6)]
[Column(TypeName = "varchar")]
public string Password { get; set; }
// 状态字段:只能存0或1
[Range(0, 1)] // 值范围约束
[Column(TypeName = "int")]
public int Status { get; set; }
[Range(0, 1)]
[Column(TypeName = "int")]
public int Status1 { get; set; }
// 重点:不映射到数据库
[NotMapped]
public int UserName { get; set; }
}
所有特性标签作用总结(必背)
-
Table("表名"):指定实体对应数据库表名
-
Key:指定主键字段
-
DatabaseGenerated(Identity):设置自增主键
-
Required:非空约束(NOT NULL)
-
MaxLength/MinLength:字符串长度限制
-
Column(TypeName="类型"):指定数据库字段类型(varchar/int)
-
Range(min,max):数值范围约束
-
NotMapped :不映射到数据库,只做程序临时字段,库中无此列
四、EF 上下文类完整解析(CodeFirst核心)
上下文作用:数据库连接、表映射、所有CRUD操作入口
internal class UserInfoModel:DbContext
{
// 构造函数:绑定配置文件中的连接字符串 name=conn
public UserInfoModel():base("name=conn")
{
}
// 数据库表映射集合
// virtual + DbSet<实体> 代表数据库的一张表
public virtual DbSet<UserInfo> UserInfos { get; set; }
}
上下文三大必须满足条件
-
继承自:DbContext
-
必须重写构造函数,绑定连接字符串
-
必须定义 DbSet<实体> 属性,表示数据库表
五、EF 迁移命令全套(考试实操必考)
打开:NuGet 程序包管理器控制台
1. 首次创建数据库(项目初始化)
-
Enable-Migrations启用迁移(整个项目只执行一次) -
Add-Migration init创建迁移记录(init=初始化数据库) -
Update-Database执行迁移,自动创建数据库+数据表+字段约束
2. 后续新增/修改字段更新数据库
-
Add-Migration 自定义名称生成新的迁移文件 -
Update-Database更新数据库结构
核心原理 :CodeFirst 不手动改数据库,改实体类 → 迁移更新库
六、WinForm 界面 CRUD 代码拆解
1. 全局上下文对象
// 全局唯一数据库上下文
UserInfoModel u1 = new UserInfoModel();
2. 数据绑定封装方法
public void BindView()
{
// 查询所有数据绑定表格
dataGridView1.DataSource = u1.UserInfos.ToList();
}
3. 窗体加载事件
private void Form1_Load(object sender, EventArgs e)
{
BindView();
// NotMapped 字段不在数据库,隐藏界面列
if (dataGridView1.Columns["UserName"]!=null)
{
dataGridView1.Columns["UserName"].Visible = false;
}
}
4. 新增功能(Button1)
private void button1_Click(object sender, EventArgs e)
{
// 1. 实例化实体
UserInfo u2 = new UserInfo()
{
Account = "123456",
Password = "Password",
Status = 1,
Status1= 0
};
// 2. 添加到EF上下文内存
u1.UserInfos.Add(u2);
// 3. 提交到数据库
int row = u1.SaveChanges();
// 4. 判断结果
if (row>0)
{
MessageBox.Show("添加成功");
BindView();
}
else
{
MessageBox.Show("添加失败");
}
}
七、CodeFirst 专属超级考点
1. NotMapped 必考
标记的属性不会生成数据库列,仅用于程序内部临时使用,数据库完全不存在该字段。
2. 自增主键配置
[Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] CodeFirst 默认非int主键不自增,必须手动加特性。
3. 迁移命令使用场景
每次修改实体类(增字段、删字段、改约束),必须执行迁移更新数据库,否则数据库结构不会同步。
4. CodeFirst 与 DBFirst 最大区别
-
DBFirst:库优先,实体由数据库生成
-
CodeFirst:代码优先,数据库由实体生成
八、易错点总结(考试扣分点)
-
忘记写 DbSet 属性,导致无法生成表
-
缺少 base("连接字符串名") 导致连接失败
-
新增字段不执行迁移,数据库不更新
-
未加 Required 允许空值,与需求不符
-
忘记 NotMapped 临时字段会多余生成数据库列
九、完整项目结构梳理
-
Domain 层:实体类 + 所有数据特性标签(表结构定义)
-
Context 层:EF上下文类(数据库连接+表映射)
-
Form 层:界面CRUD + 数据绑定刷新
配套 App.config 完整配置(CodeFirst 直接复制使用)
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<!-- name="conn" 必须和上下文构造函数 base("name=conn") 名称保持一致 -->
<add name="conn"
connectionString="Data Source=.,1344;Initial Catalog=BankSystem;User ID=sa;Password=123456"
providerName="System.Data.SqlClient"/>
</connectionStrings>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
</startup>
</configuration>