目录
2.PersonInfoDAL代码【这里是关键,有些描述在代码中说】:
数据库有2张表PersonInfo(人员信息表)、FamilyInfo(家庭成员表)。1条人员信息数据、对应多条家庭成员数据。
一、问题
在执行webapi中PersonInfo的新增接口时候,2个表中都有数据(正常);但是在执行PersonInfo的查询、修改(特别是这里)、删除接口时候,只能查询到PersonInfo(人员信息表)中的数据,FamilyInfo这里是空。如下:
2个实体代码如下:
public class PersonInfo //人员信息实体(表)
{
[Key]
public int PerId { get; set; }//自增字段
public string PerName { get; set; }//姓名
public string PeridCard { get; set; }//身份证号码
public ICollection<FamilyInfo> FamilyInfos { get; set; }//对应家庭成员,因为存在多个,所以是ICollection,为什么不是list?我们也没太关注,开始我也用的list,改成这个了。
}
public class FamilyInfo //家庭成员实体(表)
{
[Key]
public int ID { get; set; }//自增字段
public string PeridCard { get; set; }//身份证号码,这个是personinfo的身份证号码,多余了。
public string Call { get; set; }//称谓
public string Name { get; set; }//姓名
[ForeignKey("PersonInfo")]
public int PersonInfoPerId { get; set; }
}
由于我前端界面是PersonInfo(人员信息)界面,在修改人员信息的时候,要附带修改删除它的关联表FamilyInfo(家庭成员表)。
二、解决方法及步骤
解决前面问题的方法就是要配置2个实体的关联关系。
(一)思路
我是通过在PersonInfo实体中增加ICollection<FamilyInfo>,来实现关联,正确的叫法在微软官方文档中查到了叫:无需导航到主体实体且有阴影外键的一对多【就是这个实体在通过efcore的update生成数据库的时候,会在FamilyInfo 表中生成一个PersonInfoPerId 字段,如下图。这个外键没在实体中反应,所以感觉叫阴影】
数据库调用部分:
以前我写了一个通用的GenericRepository类(附在文档最后,不是此文档的重点),调用DbContext,来实现的增删改查。
现在由于PersonInfo要关联FamilyInfo,在修改、删除PersonInfo的时候要同步对FamilyInfo表进行操作,在通用GenericRepository类中配置难度有点大,所以此次单独建了一个PersonInfoDAL,来处理增删改查功能。
增删改查大体流程:
1.PersonInfo的api控制器(PersonInfoController)将PersonInfo对象(带FamilyInfos数据的)传给PersonInfoDAL,PersonInfoDAL调用DBcontext实现增删改查功能。
前端传到控制器的数据格式(PersonInfo对象):
PersonInfo
{
"PerId": 1,
"PerName": "张三11",
"PeridCard": "523123123122",
"PerTel": "1235546322",
"FamilyInfos": [
{
"ID": 1,
"PeridCard": "12122",
"Call": "父亲",
"Name": "张富11",
},
{
"ID": 2,
"PeridCard": "12122",
"Call": "母亲",
"Name": "张母11",
}
]
}
(二)步骤
1.PersonInfoController控制器代码
PersonInfoController(api控制器)代码(直接把实体或者实体的id传给personInfoDAL来处理)如下:
using BLL;
using DAL;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Unitity;
using webapi.Models;
namespace webapi.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class PersonInfoController : ControllerBase
{
private readonly PersonInfoDAL _personInfoDAL;//修改用这个类
public PersonInfoController(PersonInfoDAL personInfoDAL)
{
_personInfoDAL = personInfoDAL;
}
#region 1.[HttpPost] 新增数据
// POST: api/PersonInfo 这个接口实现增加功能
[HttpPost]
public ResponseMessage Add([FromBody] PersonInfo entity)
{
ResponseMessage responseMessage = new ResponseMessage();
try
{
_personInfoDAL.Add(entity);
responseMessage.code = 200;
responseMessage.Data = entity;
responseMessage.Message = "新增用户成功";
}
catch (Exception ex)
{
responseMessage.code = 500;
responseMessage.Data = entity;
responseMessage.Message = "新增用户失败:" + ex.Message;
}
return responseMessage;
}
#endregion
#region 2. [HttpDelete("{id}")] 删除1条数据接口
// DELETE: api/PersonInfo/ 删除接口
[HttpDelete("{id}")]
public ResponseMessage Delete(int id)
{
ResponseMessage responseMessage = new ResponseMessage();
try
{
bool result = _personInfoDAL.Delete(id);
if (result)
{
responseMessage.code = 200;
responseMessage.Data = id;
responseMessage.Message = "删除成功";
}
else
{
responseMessage.code = 300;
responseMessage.Data = id;
responseMessage.Message = "删除失败";
}
}
catch (Exception ex)
{
responseMessage.code = 500;
responseMessage.Data = id;
responseMessage.Message = "删除异常失败:" + ex.Message;
}
return responseMessage;
}
#endregion
#region 3. [HttpPut("{id}")] 修改1条数据接口
// PUT: api/PersonInfo/ 更新接口
[HttpPut("{id}")]
public ResponseMessage Update(int id, [FromBody] PersonInfo entity)
{
ResponseMessage responseMessage = new ResponseMessage();
try
{
var data = _personInfoDAL.Update(entity);
if (data)
{
responseMessage.code = 200;
responseMessage.Data = entity;
responseMessage.Message = "修改成功";
}
else
{
responseMessage.code = 300;
responseMessage.Data = entity;
responseMessage.Message = "修改失败";
}
}
catch (Exception ex)
{
responseMessage.code = 500;
responseMessage.Data = entity;
responseMessage.Message = "修改异常失败:" + ex.Message;
}
return responseMessage;
}
#endregion
#region 4.1.HttpGet接口 查询所有数据
[HttpGet]
public ResponseMessage GetAll()
{
ResponseMessage responseMessage = new ResponseMessage();
try
{
responseMessage.code = 200;
responseMessage.Data = _personInfoDAL.GetAll();
responseMessage.Message = "查询所有用户成功";
}
catch (Exception ex)
{
responseMessage.code = 500;
responseMessage.Data = null;
responseMessage.Message = "查询所有用户失败:" + ex.Message;
}
return responseMessage;
}
#endregion
#region 4.2. [HttpGet("{id}")] 查询1条数据接口
// GET: api/PersonInfo/ 查询接口
[HttpGet("{id}")]
public ResponseMessage GetById(int id)
{
ResponseMessage responseMessage = new ResponseMessage();
try
{
var data = _personInfoDAL.GetById(id);
if (data == null)
{
responseMessage.code = 404;
responseMessage.Data = null;
responseMessage.Message = "数据库没有查询到数据。";
}
else
{
responseMessage.code = 200;
responseMessage.Data = data;
responseMessage.Message = "查询所有用户成功";
}
}
catch (Exception ex)
{
responseMessage.code = 500;
responseMessage.Data = null;
responseMessage.Message = "查询所有用户失败:" + ex.Message;
}
return responseMessage;
}
#endregion
# region 4.3 分页查询
[HttpGet]
public ResponseMessage GetPageData(int pageIndex, int pageSize)
{
ResponseMessage responseMessage = new ResponseMessage();
try
{
responseMessage.code = 200;
responseMessage.Data = _personInfoDAL.GetPageData(pageIndex, pageSize); ;
responseMessage.Message = "查询所有用户成功";
}
catch (Exception ex)
{
responseMessage.code = 500;
responseMessage.Data = null;
responseMessage.Message = "查询所有用户失败:" + ex.Message;
}
return responseMessage;
}
#endregion
}
}
2.PersonInfoDAL代码【这里是关键,有些描述在代码中说】:
using Microsoft.EntityFrameworkCore;
using OpenQA.Selenium.BiDi.Modules.Log;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using webapi.Models;
namespace DAL
{
public class PersonInfoDAL
{
//这里就不多说了,注入DbContext,来操作数据库
public readonly MangeDbContext _mangeDbContext;
public PersonInfoDAL(MangeDbContext mangeDbContext)
{
_mangeDbContext = mangeDbContext;
}
//1.新增
public bool Add(PersonInfo entity)
{
_mangeDbContext.PersonInfos.Add(entity);//这里直接add PersonInfos,就会自动将FamilyInfos的数据写入FamilyInfo表,不用单独配置
return _mangeDbContext.SaveChanges() > 0 ? true : false;
}
//2.删除
public bool Delete(int Id)
{
if(Id == 0)
{
return false;
}
else
{
var person = _mangeDbContext.PersonInfos.Include(p => p.FamilyInfos).FirstOrDefault(p => p.PerId == Id);
//删除功能因为前端只传了一个id过来,所以这里要通过id把PersonInfo和关联的FamilyInfo数据查出来,这里一定要用Include,不然就只有PersonInfo的数据。
if (person == null)
{
return false;
}
else
{
_mangeDbContext.PersonInfos.Remove(person);
//这里删除PersonInfo,就会自动删除关联的FamilyInfo数据。
return _mangeDbContext.SaveChanges() > 0;
}
}
}
//3.查询所有数据
public List<PersonInfo> GetAll()
{
//查询这里,刚刚在删除功能那里已经说了,因为用到PersonInfo接口,要想查询到关联的FamilyInfo,就必须要加Include。
return _mangeDbContext.PersonInfos.Include(p => p.FamilyInfos).ToList();
}
//3.1分页查询//重新进行了优化
public async Task<PageResult<PersonInfo>> GetPageData(int pageIndex, int pageSize)
{
var query = _mangeDbContext.PersonInfos
//.Include(p => p.FamilyInfos) //前端加载的时候不用显示关联表,不然太卡了
.AsNoTracking() // 使用 AsNoTracking 提高性能,这个也是增加的,这2项设置前加载全部要6s,设置后1s
.Skip((pageIndex - 1) * pageSize)
.Take(pageSize);
var totalItems = await _mangeDbContext.PersonInfos.CountAsync();
var data = await query.Select(p => new PersonInfo
{
PerId = p.PerId,
PerName = p.PerName,
PeridCard = p.PeridCard,
PerTel = p.PerTel,
PerType = p.PerType,
PerWorkstatus = p.PerWorkstatus,
PerNational = p.PerNational,
PerJg = p.PerJg,
PerBirthplace = p.PerBirthplace,
PerRddate = p.PerRddate,
PerWorkdate = p.PerWorkdate,
PerProfessional = p.PerProfessional,
PerSpecial = p.PerSpecial,
PerEducationone = p.PerEducationone,
PerSchoolone = p.PerSchoolone,
PerEducationtwo = p.PerEducationtwo,
PerSchooltwo = p.PerSchooltwo,
PerPositionnow = p.PerPositionnow,
PerJc = p.PerJc,
PerExamine = p.PerExamine,
//PerPhoto = p.PerPhoto, // 假设 PerPhoto 是图片的 URL 太卡,查询时不用这个字段
PerEnterdate = p.PerEnterdate,
PerLeavedate = p.PerLeavedate,
FamilyInfos = null//加载的时候不用加载关联表,不然很
}).ToListAsync();
return new PageResult<PersonInfo>
{
TotalCount = totalItems,
Data = data
};
}
//3.2通过ID查询
public PersonInfo? GetById(int id)
{
return _mangeDbContext.PersonInfos.Include(p => p.FamilyInfos).FirstOrDefault(p => p.PerId == id);
}
//4.修改【这部分重新优化了一下,原来代码,在person 】
public bool Update(PersonInfo model)
2{
3 // 从数据库中查找指定ID的PersonInfo记录,并包含其关联的FamilyInfos数据
4 var person = _mangeDbContext.PersonInfos.Include(p => p.FamilyInfos).FirstOrDefault(p => p.PerId == model.PerId);
5
6 // 如果找不到对应的PersonInfo记录,则返回false
7 if (person == null)
8 {
9 return false;
10 }
11
12 // 更新PersonInfo的基本信息,这里改用的直接赋值修改方法,用SetValues的时候如果有familyinfo就会出现问题,没研究透彻,所以改用原始方法。
13 person.PerName = model.PerName;
14 person.PeridCard = model.PeridCard;
15 person.PerTel = model.PerTel;
16 person.PerType = model.PerType;
17 person.PerWorkstatus = model.PerWorkstatus;
18 person.PerNational = model.PerNational;
//personinfo和familyinfo我都只展示的一部分字段,可根据自己的修改。
19
36
37 // 处理FamilyInfo
38 // 创建一个HashSet来存储现有的FamilyInfo ID
39 var existingFamilyIds = new HashSet<int>(person.FamilyInfos.Select(f => f.ID.GetValueOrDefault()));
40
41 // 遍历传入的FamilyInfos
42 foreach (var family in model.FamilyInfos)
43 {
44 if (family.ID.HasValue) // 存在ID表示这是一个已存在的记录
45 {
46 // 查找与传入ID匹配的现有FamilyInfo
47 var existingFamily = person.FamilyInfos.FirstOrDefault(f => f.ID == family.ID.Value);
48 if (existingFamily != null)
49 {
50 // 更新现有FamilyInfo的信息
51 existingFamily.Call = family.Call; // 更新称呼
52 existingFamily.Name = family.Name; // 更新姓名
53
57 if (existingFamily.ID.HasValue)
58 {
59 // 从HashSet中移除已处理的ID
60 existingFamilyIds.Remove(existingFamily.ID.Value);
61 }
62 }
63 else
64 {
65 // 如果找不到对应的FamilyInfo,可以选择抛出异常或者跳过,此处暂不处理
66 continue;
67 }
68 }
69 else // 新增的家庭成员
70 {
71 // 创建一个新的FamilyInfo对象
72 var newFamily = new FamilyInfo
73 {
74 PeridCard = family.PeridCard, // 身份证号
75 Call = family.Call, // 称呼
76 Name = family.Name, // 姓名
77 Birthday = family.Birthday, // 生日
78 Politics = family.Politics, // 政治面貌
79 WorkPosition = family.WorkPosition, // 工作职位
80 Notes = family.Notes // 备注
81 };
82 // 将新的FamilyInfo添加到person的FamilyInfos集合中
83 person.FamilyInfos.Add(newFamily);
84 }
85 }
86
87 // 移除不在传入列表中的FamilyInfo
88 // 找到所有需要移除的FamilyInfo
89 var familiesToRemove = person.FamilyInfos.Where(f => f.ID.HasValue && existingFamilyIds.Contains(f.ID.Value)).ToList();
90 foreach (var family in familiesToRemove)
91 {
92 // 从person的FamilyInfos集合中移除这些FamilyInfo
93 person.FamilyInfos.Remove(family);
94 }
95
96 // 保存更改到数据库,并返回是否成功
97 return _mangeDbContext.SaveChanges() > 0;
98}
修改功能展示:
后端personinfoDAL类执行修改(详见上述代码)。
3.DBcontext代码:
namespace DAL
{
public class MangeDbContext : DbContext
{
public MangeDbContext(DbContextOptions<MangeDbContext> options) : base(options)
{
// 启用懒加载
this.ChangeTracker.LazyLoadingEnabled = true;
}
public virtual DbSet<PersonInfo> PersonInfos { get; set; }
public virtual DbSet<FamilyInfo> FamilyInfos { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 配置1对多关系,并启用级联删除
modelBuilder.Entity<PersonInfo>()
.HasMany(e => e.FamilyInfos)
.WithOne()
.IsRequired()
.HasForeignKey(f => f.PersonInfoPerId)
.OnDelete(DeleteBehavior.Cascade);//启用联级删除
}
}
}
4.测试环节【我实体属性上面比例子要多些,不用纠结】:
新增接口测试:
修改接口测试:
删除接口测试:
综上已通过1个接口,实现1对多增删改查。
5.附:GenericRepository通用类代码
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
using webapi.Models;
namespace DAL
{
public class GenericRepository<T> where T : class
{
private readonly MangeDbContext _context;
private readonly DbSet<T> _dbSet;
public GenericRepository(MangeDbContext context)
{
_context = context;
_dbSet = _context.Set<T>();
}
#region 1.增加
/// <summary>
/// 1.增加
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public bool Add(T entity)
{
_dbSet.Add(entity);
return _context.SaveChanges() > 0 ? true : false;
}
#endregion
#region 2.删
public bool Delete(int id)
{
var _entity = _dbSet.Find(id);
if (_entity != null)
{
_dbSet.Remove(_entity);
return _context.SaveChanges() > 0 ? true : false;
}
else
{
return false;
}
}
#endregion
#region 3.改
public bool Update(T entity)
{
if (entity != null)
{
//_dbSet.Update(entity);
_dbSet.Entry(entity).State = EntityState.Modified;
return _context.SaveChanges() > 0 ? true : false;
}
else
{
return false;
}
}
#endregion
#region 4.查所有
public IEnumerable<T> GetAll()
{
return _dbSet.ToList();
}
#endregion
#region 4.1查byid
public T? GetById(int id)
{
return _dbSet.Find(id);
}
#endregion
#region 4.2分页查询
public List<T> GetPageData(int pageIndex, int pageSize)
{
return _dbSet.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
}
#endregion
}
}
三、最开始想想法及实践【修改功能没成功,可参考思路】
最最最下面是最开始发现这个功能不好实现的想法和实践
主要是想在所有控制器都通用的GenericRepository类中来判断传进来的是否是PersonInfo来单独处理,貌似没成功。下面的代码personinfo接口修改有问题(估计遍历familyinfo能够解决,不想弄了),大家可以看看这个思路。
调用PersonInfo的Add新增接口的时候,正常,图片如下:
这里最后一个字段为空,应该就是这里没关联到。
调用PersonInfo的GetAll查询接口的时候,不正常,不显示依赖表FamilyInfo的数据,图片如下:
1.解决办法:(1)在DbContext的时候,要配置关联查询。我用的通用类GenericRepository来实现的增删改查,所以我这个代码在GenericRepository中来修改的。【做到一半发现,1对多这个表PersonInfo的修改、删除功能,都得增加代码才行,下面是已加代码可用版本】
具体如下(加粗标注的是新增的代码):
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
using webapi.Models;
namespace DAL
{
public class GenericRepository<T> where T : class
{
private readonly MangeDbContext _context;
private readonly DbSet<T> _dbSet;
public GenericRepository(MangeDbContext context)
{
_context = context;
_dbSet = _context.Set<T>();
}
#region 1.增加
#region 2.删
public bool Delete(int id)
{
if (typeof(T).Name == "PersonInfo")
{
return DeletePersonInfoAndFamily(id);
}
else
{
var _entity = _dbSet.Find(id);
if (_entity != null)
{
_dbSet.Remove(_entity);
return _context.SaveChanges() > 0 ? true : false;
}
else
{
return false;
}
}
}
public bool DeletePersonInfoAndFamily(int perId)
{
var person = _context.PersonInfos.Include(p => p.FamilyInfos).FirstOrDefault(p => p.PerId == perId);
if (person != null)
{
foreach (var family in person.FamilyInfos.ToList())
{
_context.FamilyInfos.Remove(family);
}
_context.Remove(person);
return _context.SaveChanges() > 0;
}
else
{
return false;
}
}
#endregion
#region 3.改 这个接口也得重写,弄了一中午总算搞定了。
public bool Update(T entity)
{
if (typeof(T).Name == "PersonInfo")
{
return UpdatePersonInfo(entity as PersonInfo);
}
_dbSet.Entry(entity).State = EntityState.Modified;
return _context.SaveChanges() > 0 ? true : false;
}
#endregion
private bool UpdatePersonInfo(PersonInfo personinfo)
{
if (personinfo == null)
{
return false;
}
var personInfoDbSet = _dbSet as DbSet<PersonInfo>;
if (personInfoDbSet == null)
{
return false;
}
// 1. 先查询出原始的PersonInfo实体并加载关联的FamilyInfos数据,同时开启跟踪
var originalPersonInfo = personInfoDbSet.AsTracking().Include(p => p.FamilyInfos)
.FirstOrDefault(p => p.PerId == personinfo.PerId);
if (originalPersonInfo == null)
{
return false;
}
// 处理FamilyInfos集合中单个元素的变更,标记每个元素为已修改状态
foreach (var familyInfo in personinfo.FamilyInfos)
{
var existingFamilyInfo = originalPersonInfo.FamilyInfos.FirstOrDefault(f => f.ID == familyInfo.ID);
if (existingFamilyInfo != null)
{
// 通过上下文对象获取FamilyInfo对应的实体入口,用于更新其属性值
var familyInfoEntry = _context.Entry(existingFamilyInfo);
familyInfoEntry.CurrentValues.SetValues(familyInfo);
}
else
{
originalPersonInfo.FamilyInfos.Add(familyInfo);
}
}
// 设置主实体PersonInfo的状态为已修改
personInfoDbSet.Entry(originalPersonInfo).State = EntityState.Modified;
// 保存更改并根据影响行数判断是否更新成功
return _context.SaveChanges() > 0 ? true : false;
}
#region 4.查
public IEnumerable<T> GetAll()
{
if (typeof(T).Name == "PersonInfo")//在执行查询的时候,判断一下传入的是不是“PersonInfo”类,其他类没有关联表按正常返回即可。
{
return (IEnumerable<T>)GetPersonInfoAll();//如果是PersonInfo类,就调用单独的关联查询方法(在下面)
}
return _dbSet.ToList();
}
private IEnumerable<PersonInfo> GetPersonInfoAll()
{
return _dbSet.OfType<PersonInfo>().Include(p => p.FamilyInfos).ToList();//这里就是配置关联的关键语句
}
public T? GetById(int id)
{
if (typeof(T).Name == "PersonInfo")//通过Id查询,与上面同理。
{
return (T)(object)GetPersonInfoById(id);
}
return _dbSet.Find(id);
}
private PersonInfo GetPersonInfoById(int id)
{
return _dbSet.OfType<PersonInfo>().Include(p => p.FamilyInfos).FirstOrDefault();
}
}
}
(2)在DbContext中启用懒加载
public class MangeDbContext : DbContext
{
public MangeDbContext(DbContextOptions<MangeDbContext> options) : base(options)
{
// 启用懒加载
this.ChangeTracker.LazyLoadingEnabled = true;
}
public virtual DbSet<FamilyInfo> FamilyInfos { get; set; }
public virtual DbSet<PersonInfo> PersonInfos { get; set; }
}
}
删除数据库中数据,重新之心Add接口后,FamilyInfo表中自动生成的的关联字段有数据。
再执行GetAll查询接口,正常:
2.另外需要注意的地方:上述方法的2个实体表,我没有配置关联字段,系统自动在FamilyInfo表创建时增加了1个字段PersonInfoPerId,PerId是PersonInfo表的自增字段。但是你如果自己想另外用一个自己配置的字段作为关联,就需要在DbContext中的OnModelCreating中配置,具体代码如下:
public MangeDbContext(DbContextOptions<MangeDbContext> options) : base(options)
{
// 启用懒加载
this.ChangeTracker.LazyLoadingEnabled = true;
}
public virtual DbSet<FamilyInfo> FamilyInfos { get; set; }
public virtual DbSet<PersonInfo> PersonInfos { get; set; }
//添加如下代码
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PersonInfo>()
.HasMany(p => p.FamilyInfos)
.WithOne()
.HasForeignKey(f => f.PeridCard )
.HasPrincipalKey(p => p.PeridCard );
}
}
四、官方文档,深入理解一对多关系
突然发现官方文档,留存以备后续使用查看:
一对多关系
-
项目
-
2023/04/17
-
4 个参与者
反馈
本文内容
显示另外 8 个
当单个实体与任意数量的其他实体关联时,将使用一对多关系。 例如,Blog
可以有多个关联的 Posts
,但每个 Post
都只与一个 Blog
相关联。
本文档采用围绕大量示例展开的结构。 这些示例从常见情况着手,还引入了一些概念。 后面的示例介绍了不太常见的配置类型。 此处介绍了一个不错的方法,即了解前几个示例和概念,再根据特定需求转到后面的示例。 基于此方法,我们将从简单的“必需”和“可选”的一对多关系开始。
提示
可在 OneToMany.cs 中找到以下所有示例的代码。
必需的一对多
C#复制
// Principal (parent)
public class Blog
{
public int Id { get; set; }
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public int BlogId { get; set; } // Required foreign key property
public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}
一对多关系由以下部分组成:
- 主体实体上的一个或多个主键或备用键属性,即关系的“一”端。 例如
Blog.Id
。 - 依赖实体上的一个或多个外键属性,即关系的“多”端。 例如
Post.BlogId
。 - (可选)引用依赖实体的主体实体上的集合导航。 例如
Blog.Posts
。 - (可选)引用主体实体的依赖实体上的引用导航。 例如
Post.Blog
。
因此,对于此示例中的关系:
- 外键属性
Post.BlogId
不可为空。 这会使关系成为“必需”关系,因为每个依赖实体 (Post
) 必须与某个主体实体 (Blog
) 相关,而其外键属性必须设置为某个值。 - 这两个实体都有指向关系另一端的相关实体的导航。
备注
必需的关系可确保每个依赖实体都必须与某个主体实体相关联。 但是,主体实体可以在没有任何依赖实体的情况下始终存在。 也就是说,必需的关系并不表示始终存在至少一个依赖实体。 无论是在 EF 模型,还是在关系数据库中,都没有确保主体实体与特定数量的依赖实体相关联的标准方法。 如果需要,则必须在应用程序(业务)逻辑中实现它。 有关详细信息,请参阅必需的导航。
提示
具有两个导航的关系(一个是从依赖实体到主体实体,一个是从主体实体到依赖实体)称为双向关系。
此关系按约定发现。 即:
Blog
作为关系中的主体实体被发现,Post
作为依赖实体被发现。Post.BlogId
作为引用主体实体的Blog.Id
主键的依赖实体的外键被发现。 由于Post.BlogId
不可为空,所以发现这一关系是必需的。Blog.Posts
作为集合导航被发现。Post.Blog
作为引用导航被发现。
重要
使用 C# 可为空引用类型时,如果外键属性可为空,则引用导航必须不可为空。 如果外键属性不可为空,则引用导航可以为空,也可以不为空。 在这种情况下,Post.BlogId
和 Post.Blog
皆不可为空。 = null!;
构造用于将此标记为 C# 编译器的有意行为,因为 EF 通常会对 Blog
实例进行设置,并且对于完全加载的关系,它不能为空。 有关详细信息,请参阅使用可为空引用类型。
对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:
C#复制
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne(e => e.Blog)
.HasForeignKey(e => e.BlogId)
.IsRequired();
}
在上面的示例中,关系的配置从 主体实体类型 (Blog
) 上的 HasMany
开始,然后是 WithOne
。 与所有关系一样,它完全等效于从依赖实体类型开始 (Post
),然后依次使用 HasOne
和 WithMany
。 例如:
C#复制
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(e => e.Blog)
.WithMany(e => e.Posts)
.HasForeignKey(e => e.BlogId)
.IsRequired();
}
与另一个选项相比,这两个选项并没有什么优势:它们都会导致完全相同的配置。
提示
没有必要对关系进行两次配置,即先从主体实体开始,又从依赖实体开始。 此外,尝试单独配置关系的主体实体和依赖实体通常不起作用。 选择从一端或另一端配置每个关系,然后只编写一次配置代码。
可选的一对多
C#复制
// Principal (parent)
public class Blog
{
public int Id { get; set; }
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public int? BlogId { get; set; } // Optional foreign key property
public Blog? Blog { get; set; } // Optional reference navigation to principal
}
这与上一个示例相同,只不过外键属性和到主体实体的导航现在可为空。 这会使关系成为“可选”关系,因为依赖实体 (Post
) 可以在无需与任何主体实体 (Blog
) 相关的情况下存在。
重要
使用 C# 可为空引用类型时,如果外键属性可为空,则引用导航必须不可为空。 在本例中,Post.BlogId
不可为空,因此 Post.Blog
也不得为空。 有关详细信息,请参阅使用可为空引用类型。
如前所述,此关系按约定发现。 对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:
C#复制
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne(e => e.Blog)
.HasForeignKey(e => e.BlogId)
.IsRequired(false);
}
1.具有阴影外键的必需的一对多
C#复制
// Principal (parent)
public class Blog
{
public int Id { get; set; }
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}
在某些情况下,你可能不需要模型中的外键属性,因为外键是关系在数据库中表示方式的详细信息,而在完全以面向对象的方式使用关系时,不需要外键属性。 但是,如果要序列化实体(例如通过网络发送),则当实体不采用对象形式时,外键值可能是保持关系信息不变的有用方法。 因此,为实现此目的,务实的做法是在 .NET 类型中保留外键属性。 外键属性可以是私有的,这是一个折中的办法,既可以避免公开外键,又允许其值随实体一起传输。
继前面的两个示例后,此示例从依赖实体类型中删除外键属性。 EF 因此会创建一个名为 BlogId
的 int
类型的阴影外键属性。
此处需要注意的一个要点是,正在使用 C# 可为空引用类型,因此引用导航的可为空性用于确定外键属性是否可为空,进而确定关系是可选的还是必需的。 如果未使用可为空引用类型,则默认情况下,阴影外键属性将为空,使关系默认为可选。 在这种情况下,使用 IsRequired
强制阴影外键属性为不可为空,并使关系成为必需关系。
如前所述,此关系按约定发现。 对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:
C#复制
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne(e => e.Blog)
.HasForeignKey("BlogId")
.IsRequired();
}
2.具有阴影外键的可选一对多
C#复制
// Principal (parent)
public class Blog
{
public int Id { get; set; }
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public Blog? Blog { get; set; } // Optional reference navigation to principal
}
与前面的示例一样,外键属性已从依赖实体类型中移除。 EF 因此会创建一个名为 BlogId
的 int?
类型的阴影外键属性。 与前面的示例不同,这次外键属性创建为可为空,因为正在使用 C# 可为空引用类型,并且依赖实体类型的导航可为空。 这会使关系成为可选关系。
如果未使用 C# 可为空引用类型,则默认情况下,外键属性也将创建为可为空。 这意味着,与自动创建的阴影属性的关系默认可选。
如前所述,此关系按约定发现。 对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:
C#复制
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne(e => e.Blog)
.HasForeignKey("BlogId")
.IsRequired(false);
}
3.无需导航到主体实体的一对多
C#复制
// Principal (parent)
public class Blog
{
public int Id { get; set; }
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public int BlogId { get; set; } // Required foreign key property
}
在此示例中,外键属性已重新引入,但依赖实体上的导航已被移除。
提示
只有一个导航的关系(即从依赖实体到主体实体,或从主体实体到依赖实体,但只有其中一个)的被称单向关系。
如前所述,此关系按约定发现。 对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:
C#复制
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne()
.HasForeignKey(e => e.BlogId)
.IsRequired();
}
请注意,对 WithOne
的调用没有参数。 这是告知 EF 没有从 Post
到 Blog
的导航的方式。
如果从没有导航的实体开始配置,则必须使用泛型 HasOne<>()
调用显式指定关系另一端的实体类型。 例如:
C#复制
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne<Blog>()
.WithMany(e => e.Posts)
.HasForeignKey(e => e.BlogId)
.IsRequired();
}
4.无需导航到主体实体且有阴影外键的一对多
C#复制
// Principal (parent)
public class Blog
{
public int Id { get; set; }
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
}
此示例通过移除外键属性和依赖实体上的导航来合并上述两个示例。
此关系按约定作为可选关系被发现。 由于代码中没有任何内容可以用来指示它是必需的,因此需要使用 IsRequired
进行最小配置来创建必需关系。 例如:
C#复制
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne()
.IsRequired();
}
可以使用更完整的配置来显式配置导航和外键名称,并根据需要对 IsRequired()
或 IsRequired(false)
进行适当的调用。 例如:
C#复制
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne()
.HasForeignKey("BlogId")
.IsRequired();
}
5.无需导航到依赖实体的一对多
C#复制
// Principal (parent)
public class Blog
{
public int Id { get; set; }
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public int BlogId { get; set; } // Required foreign key property
public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}
前两个示例具有从主体实体到依赖实体的导航,但没有从依赖实体到主体实体的导航。 在接下来的几个示例中,将重新引入依赖实体上的导航,而主体上的导航将被移除。
如前所述,此关系按约定发现。 对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:
C#复制
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(e => e.Blog)
.WithMany()
.HasForeignKey(e => e.BlogId)
.IsRequired();
}
请注意,WithMany()
调用时没有参数,可指示此方向没有导航。
如果从没有导航的实体开始配置,则必须使用泛型 HasMany<>()
调用显式指定关系另一端的实体类型。 例如:
C#复制
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany<Post>()
.WithOne(e => e.Blog)
.HasForeignKey(e => e.BlogId)
.IsRequired();
}
6.无导航的一对多
有时,配置无导航的关系可能很有用。 此类关系只能通过直接更改外键值来操作。
C#复制
// Principal (parent)
public class Blog
{
public int Id { get; set; }
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public int BlogId { get; set; } // Required foreign key property
}
此关系不会按约定发现,因为没有任何导航指示这两种类型是相关的。 可以在 OnModelCreating
中显式配置它。 例如:
C#复制
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany<Post>()
.WithOne();
}
使用此配置时,按照约定,Post.BlogId
属性仍被检测为外键,并且关系是必需的,因为外键属性不可为空。 通过将外键属性设为“可为空”,可以使关系成为“可选”关系。
此关系的更完整显式配置是:
C#复制
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany<Post>()
.WithOne()
.HasForeignKey(e => e.BlogId)
.IsRequired();
}
7.具有备用键的一对多
在到目前为止的所有示例中,依赖实体上的外键属性被约束为主体实体上的主键属性。 外键可以改为被约束为不同的属性,该属性随后成为主体实体类型的备用键。 例如:
C#复制
// Principal (parent)
public class Blog
{
public int Id { get; set; }
public int AlternateId { get; set; } // Alternate key as target of the Post.BlogId foreign key
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public int BlogId { get; set; } // Required foreign key property
public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}
此关系不是按约定发现的,因为 EF 始终按照约定创建与主键的关系。 可以使用对 HasPrincipalKey
的调用在 OnModelCreating
中显式配置它。 例如:
C#复制
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne(e => e.Blog)
.HasPrincipalKey(e => e.AlternateId);
}
HasPrincipalKey
可与其他调用结合使用,以显式配置导航、外键属性和必需/可选性质。 例如:
C#复制
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne(e => e.Blog)
.HasPrincipalKey(e => e.AlternateId)
.HasForeignKey(e => e.BlogId)
.IsRequired();
}
8.具有复合外键的一对多
在到目前为止的所有示例中,主体实体的主键或备用键属性由单个属性组成。 利用多个属性也可以形成主键或备用键,这些键称为“组合键”。 当关系的主体实体具有组合键时,依赖实体的外键也必须是具有相同属性数的组合键。 例如:
C#复制
// Principal (parent)
public class Blog
{
public int Id1 { get; set; } // Composite key part 1
public int Id2 { get; set; } // Composite key part 2
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public int BlogId1 { get; set; } // Required foreign key property part 1
public int BlogId2 { get; set; } // Required foreign key property part 2
public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}
此关系按约定发现。 但是,需要显式配置组合键本身:
C#复制
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasKey(e => new { e.Id1, e.Id2 });
}
重要
如果组合外键值的任何属性值为空,则认为其值为 null
。 具有一个属性“空”和另一个属性“非空”的组合外键不会被视为与具有相同值的主键或备用键匹配。 两者都将被视为 null
。
HasForeignKey
和 HasPrincipalKey
都可用于显式指定具有多个属性的键。 例如:
C#复制
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>(
nestedBuilder =>
{
nestedBuilder.HasKey(e => new { e.Id1, e.Id2 });
nestedBuilder.HasMany(e => e.Posts)
.WithOne(e => e.Blog)
.HasPrincipalKey(e => new { e.Id1, e.Id2 })
.HasForeignKey(e => new { e.BlogId1, e.BlogId2 })
.IsRequired();
});
}
提示
在上面的代码中,对 HasKey
和 HasMany
的调用已组合到嵌套生成器中。 使用嵌套生成器,无需为同一实体类型多次调用 Entity<>()
,但在功能上等效于多次调用 Entity<>()
。
9.无需级联删除的一对多
C#复制
// Principal (parent)
public class Blog
{
public int Id { get; set; }
public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}
// Dependent (child)
public class Post
{
public int Id { get; set; }
public int BlogId { get; set; } // Required foreign key property
public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}
按照约定,必需关系配置为级联删除,这意味着,删除主体实体后,也会删除其所有依赖实体,因为依赖实体无法存在于每月主体实体的数据库中。 可以将 EF 配置为引发异常,而不是自动删除不再存在的依赖行:
C#复制
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(e => e.Posts)
.WithOne(e => e.Blog)
.OnDelete(DeleteBehavior.Restrict);
}
10.自引用一对多
在前面的所有示例中,主体实体类型与依赖实体类型有所不同。 情况不一定如此。 例如,在下面的类型中,每个 Employee
都与另一个 Employees
相关。
C#复制
public class Employee
{
public int Id { get; set; }
public int? ManagerId { get; set; } // Optional foreign key property
public Employee? Manager { get; set; } // Optional reference navigation to principal
public ICollection<Employee> Reports { get; } = new List<Employee>(); // Collection navigation containing dependents
}
此关系按约定发现。 对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:
C#复制
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Employee>()
.HasOne(e => e.Manager)
.WithMany(e => e.Reports)
.HasForeignKey(e => e.ManagerId)
.IsRequired(false);
}