【已解决】c#后端webapi+efcore,1个接口实现一对多的增删改查。

目录

一、问题

二、解决方法及步骤

(一)思路

(二)步骤

1.PersonInfoController控制器代码

2.PersonInfoDAL代码【这里是关键,有些描述在代码中说】:

3.DBcontext代码:

4.测试环节【我实体属性上面比例子要多些,不用纠结】:

5.附:GenericRepository通用类代码

三、最开始想想法及实践【修改功能没成功,可参考思路】

四、官方文档,深入理解一对多关系

1.具有阴影外键的必需的一对多

2.具有阴影外键的可选一对多

3.无需导航到主体实体的一对多

4.无需导航到主体实体且有阴影外键的一对多

5.无需导航到依赖实体的一对多

6.无导航的一对多

7.具有备用键的一对多

8.具有复合外键的一对多

9.无需级联删除的一对多

10.自引用一对多


数据库有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 个参与者

反馈

本文内容

  1. 必需的一对多
  2. 可选的一对多
  3. 具有阴影外键的必需的一对多
  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);
}

<think>在WMS(仓库管理系统)中,库存调拨模块通常涉及两个主要实体:调拨单(TransferOrder)调拨明细(TransferDetail)。一个调拨单包含个调拨明细(即个产品)。这是一个典型的一对关系。 我们将设计两个类: 1. TransferOrder(调拨单主) 2. TransferDetail(调拨明细,包含产品信息、数量等) 数据库结构可能如下: - TransferOrder :Id(主键)、调拨单号、调出仓库、调入仓库、调拨日期、状态等。 - TransferDetail :Id(主键)、TransferOrderId(外键)、ProductId、调拨数量、批次号等。 在代码中,我们可以使用实体类来示这些,并使用Entity Framework Core(ORM)来操作数据库。 假设我们使用ASP.NET Core Web API,并采用Repository模式(为了代码清晰可维护性),但为了简化,这里我们直接使用DbContext进行演示。 步骤: 1. 定义实体类。 2. 创建DbContext。 3. 实现增删改查(CRUD)操作。 注意:为了高效,我们使用异步编程。 首先,定义实体类: ```csharp public class TransferOrder { public int Id { get; set; } public string TransferOrderNumber { get; set; } // 调拨单号 public int FromWarehouseId { get; set; } // 调出仓库ID public int ToWarehouseId { get; set; } // 调入仓库ID public DateTime TransferDate { get; set; } // 调拨日期 public string Status { get; set; } // 状态 // 导航属性:一个调拨单有个调拨明细 public ICollection<TransferDetail> TransferDetails { get; set; } } public class TransferDetail { public int Id { get; set; } public int TransferOrderId { get; set; } // 外键 public int ProductId { get; set; } // 产品ID public int Quantity { get; set; } // 调拨数量 public string BatchNumber { get; set; } // 批次号 // 导航属性:属于一个调拨单 public TransferOrder TransferOrder { get; set; } } ``` 然后,创建DbContext: ```csharp public class AppDbContext : DbContext { public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } public DbSet<TransferOrder> TransferOrders { get; set; } public DbSet<TransferDetail> TransferDetails { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { // 配置关系(可选,EF Core默认约定已经可以识别) modelBuilder.Entity<TransferOrder>() .HasMany(o => o.TransferDetails) .WithOne(d => d.TransferOrder) .HasForeignKey(d => d.TransferOrderId) .OnDelete(DeleteBehavior.Cascade); // 设置级联删除,当主单删除时,明细自动删除 } } ``` 接下来,我们编写一个服务类(或Repository)来处理调拨单的CRUD操作。 为了高效方便,我们使用以下方法: - 新增:同时添加主单明细 - 修改:更新主单信息,同时更新明细(可能包括明细的增删改) - 删除:删除主单(级联删除明细) - 查询:根据主单ID获取主单及其所有明细 这里,我们使用一个服务类TransferOrderService: ```csharp public class TransferOrderService { private readonly AppDbContext _context; public TransferOrderService(AppDbContext context) { _context = context; } // 新增调拨单(包含明细) public async Task CreateTransferOrderAsync(TransferOrder transferOrder) { _context.TransferOrders.Add(transferOrder); await _context.SaveChangesAsync(); } // 修改调拨单(包括更新明细) // 注意:修改时,我们假设前端传递了完整的调拨单对象(包括修改后的明细) // 策略:先删除原有明细,再添加新的明细(或者根据情况更新,这里采用简单方式:全删全增) public async Task UpdateTransferOrderAsync(TransferOrder transferOrder) { // 先查询出原有的调拨单明细 var existingOrder = await _context.TransferOrders .Include(o => o.TransferDetails) // 包含明细 .FirstOrDefaultAsync(o => o.Id == transferOrder.Id); if (existingOrder == null) { throw new Exception("调拨单不存在"); } // 更新主单信息(除了明细) _context.Entry(existingOrder).CurrentValues.SetValues(transferOrder); // 删除原有明细 _context.TransferDetails.RemoveRange(existingOrder.TransferDetails); // 添加新的明细 foreach (var detail in transferOrder.TransferDetails) { // 注意:这里需要重置明细的Id(如果前端传递了Id,我们也要忽略,因为要重新生成) detail.Id = 0; // 如果数据库是自增主键,设置为0示新增 existingOrder.TransferDetails.Add(detail); } await _context.SaveChangesAsync(); } // 删除调拨单(级联删除明细) public async Task DeleteTransferOrderAsync(int transferOrderId) { var order = await _context.TransferOrders.FindAsync(transferOrderId); if (order != null) { _context.TransferOrders.Remove(order); await _context.SaveChangesAsync(); } } // 根据ID获取调拨单(包含明细) public async Task<TransferOrder> GetTransferOrderByIdAsync(int id) { return await _context.TransferOrders .Include(o => o.TransferDetails) .FirstOrDefaultAsync(o => o.Id == id); } // 获取所有调拨单(不包含明细,通常列不需要明细) public async Task<List<TransferOrder>> GetAllTransferOrdersAsync() { return await _context.TransferOrders.ToListAsync(); } } ``` 注意:在更新操作中,我们采用了先删除原有明细再新增的方式。这种方式简单,但可能会带来性能问题(如果明细很),并且会丢失原有明细的ID(如果明细有其他关联,需要谨慎)。另一种方式是逐个比较明细,进行更新、删除添加,这样更高效但更复杂。 优化更新操作(可选): 我们可以使用以下策略: 1. 遍历现有明细,如果新明细中不存在(根据ID判断),则删除。 2. 遍历新明细: - 如果明细有ID且存在于现有明细中,则更新。 - 如果明细没有ID(或ID为0),则添加。 3. 注意:如果前端传递的明细中没有包含所有原有明细,那么缺失的将被删除。 但是,为了简化,上面的例子使用了全删全增。在实际应用中,如果明细数量不,可以接受。 最后,在控制器中调用这些服务方法。 注意:在实际项目中,你可能需要DTO(数据传输对象)来传递数据,避免循环引用等问题。这里为了简化,直接使用实体类。 效率说明: - 使用异步操作提高吞吐量。 - 在查询时,使用Include来加载关联数据,避免延迟加载带来的次查询(N+1问题)。 - 在更新时,全删全增可能不是最高效的,但代码简单。对于大量明细,建议使用更精细的更新策略。 相关问题: §§相关问题§§: 1. 在Entity Framework Core中,如何配置一对的关系? 2. 在更新操作中,全删全增明细的方式有什么优缺点?有没有更好的方法? 3. 如何避免在序列化实体类时出现循环引用问题? 4. 在删除主实体时,如何实现级联删除?在EF Core中如何配置?</think>### 库存调拨模块的C#实现1关系) 以下是针对WMS库存调拨模块的C#实现,采用Entity Framework Core作为ORM框架,包含高效的增删改查操作,并遵循最佳实践。 #### 1. 实体类定义(主+明细) ```csharp // 调拨单主 public class TransferOrder { public int Id { get; set; } public string OrderNumber { get; set; } = Guid.NewGuid().ToString("N").Substring(0, 10).ToUpper(); // 自动生成单号 public int FromWarehouseId { get; set; } public int ToWarehouseId { get; set; } public DateTime TransferDate { get; set; } = DateTime.UtcNow; public string Status { get; set; } = "Pending"; // 导航属性 (1) public ICollection<TransferItem> Items { get; set; } = new List<TransferItem>(); } // 调拨明细 public class TransferItem { public int Id { get; set; } public int ProductId { get; set; } public int Quantity { get; set; } public string BatchNumber { get; set; } // 外键 public int TransferOrderId { get; set; } public TransferOrder TransferOrder { get; set; } } ``` #### 2. DbContext配置(启用级联删除) ```csharp public class AppDbContext : DbContext { public DbSet<TransferOrder> TransferOrders { get; set; } public DbSet<TransferItem> TransferItems { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { // 配置1关系 + 级联删除 modelBuilder.Entity<TransferOrder>() .HasMany(o => o.Items) .WithOne(i => i.TransferOrder) .HasForeignKey(i => i.TransferOrderId) .OnDelete(DeleteBehavior.Cascade); // 主单删除时自动删除明细 // 索引优化 modelBuilder.Entity<TransferOrder>() .HasIndex(o => o.OrderNumber) .IsUnique(); } } ``` #### 3. 仓储服务类(高效CRUD操作) ```csharp public class TransferService { private readonly AppDbContext _context; public TransferService(AppDbContext context) { _context = context; } // 新增调拨单(事务操作) public async Task<TransferOrder> CreateOrderAsync(TransferOrder order) { using var transaction = await _context.Database.BeginTransactionAsync(); try { _context.TransferOrders.Add(order); await _context.SaveChangesAsync(); await transaction.CommitAsync(); return order; } catch { await transaction.RollbackAsync(); throw; } } // 高效更新(避免全量查询) public async Task UpdateOrderAsync(int orderId, Action<TransferOrder> updateAction) { var order = await _context.TransferOrders .Include(o => o.Items) // 显式加载明细 .FirstOrDefaultAsync(o => o.Id == orderId); if (order == null) throw new KeyNotFoundException("调拨单不存在"); updateAction(order); // 外部传入更新逻辑 await _context.SaveChangesAsync(); } // 删除调拨单(级联删除明细) public async Task DeleteOrderAsync(int orderId) { // 高效删除:仅需主键 var order = new TransferOrder { Id = orderId }; _context.TransferOrders.Attach(order); _context.TransferOrders.Remove(order); await _context.SaveChangesAsync(); } // 查询优化:按需加载 public async Task<TransferOrder> GetOrderDetailsAsync(int orderId) { return await _context.TransferOrders .AsNoTracking() // 只读场景提升性能 .Include(o => o.Items) .FirstOrDefaultAsync(o => o.Id == orderId); } // 分页查询(主单列) public async Task<(List<TransferOrder>, int)> GetOrdersPagedAsync(int page, int pageSize) { var query = _context.TransferOrders .AsNoTracking() .OrderByDescending(o => o.TransferDate); int totalCount = await query.CountAsync(); var orders = await query .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); return (orders, totalCount); } } ``` #### 4. 使用示例(控制器层) ```csharp [ApiController] [Route("api/transfers")] public class TransferController : ControllerBase { private readonly TransferService _service; public TransferController(TransferService service) { _service = service; } [HttpPost] public async Task<IActionResult> Create([FromBody] TransferOrder order) { var created = await _service.CreateOrderAsync(order); return CreatedAtAction(nameof(Get), new { id = created.Id }, created); } [HttpPut("{id}")] public async Task<IActionResult> Update(int id, [FromBody] TransferOrder input) { await _service.UpdateOrderAsync(id, order => { order.FromWarehouseId = input.FromWarehouseId; order.ToWarehouseId = input.ToWarehouseId; order.Status = input.Status; // 明细更新策略:清空后重新添加 order.Items.Clear(); foreach (var item in input.Items) { order.Items.Add(new TransferItem { ProductId = item.ProductId, Quantity = item.Quantity, BatchNumber = item.BatchNumber }); } }); return NoContent(); } } ``` ### 关键设计解释: 1. **高效更新策略**: - 使用`Attach`+状态变更代替全量查询(删除场景) - 通过`Action委托`实现局部更新,避免过度查询 - 明细更新采用"清空+重建"模式,简化复杂变更逻辑 2. **性能优化**: - `AsNoTracking()`提升只读查询性能 - 分页查询使用`Skip/Take`而非获取全量数据 - 事务处理确保数据一致性 3. **级联删除**: - 通过`OnDelete(DeleteBehavior.Cascade)`配置 - 删除主单时自动删除所有关联明细 - 避免手动循环删除导致的低效操作 4. **并发控制**: - 使用事务保证原子操作(如创建操作) - 更新时采用"先查询后更新"模式处理业务规则 5. **索引优化**: - 为调拨单号(OrderNumber)添加唯一索引 - 按日期倒序排序提高最近单据查询速度
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值