EntityFramework 外键值映射

如果在 EF OnModelCreating 中配置了实体外键映射,也就是 SQL Server 中的 ForeignKey,那么我们在添加实体的时候,主实体的主键值会自动映射到子实体的外键值,并且这个操作在一个 SaveChanges 中,但如果没有在 OnModelCreating 中进行外键映射配置,我们添加实体的时候,就不会自动映射外键值了,什么意思呢?我们先看一个示例代码:

public class SchoolDbContext : DbContext{    public SchoolDbContext()
        : base("db_school")
    { }    public DbSet<Student> Students { get; set; }    public DbSet<Class> Classs { get; set; }    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Student>()
            .HasKey(n => n.StudentId);

        modelBuilder.Entity<Class>()
            .HasKey(n => n.ClassId);        base.OnModelCreating(modelBuilder);
    }
}public class Student{    public int StudentId { get; set; }    public int ClassId { get; set; }    public string Name { get; set; }
}public class Class{    public int ClassId { get; set; }    public string Name { get; set; }
}

示例很简单,Class 和 Student 是一对多关系,但我们并没有在 OnModelCreating 中进行外键映射配置,所以生成到 SQL Server 的 db_school 数据库,会是这样:

可以看到 Student 中的 ClassId 字段并不是外键,下面我们添加 Student 和 Class 实体:

static void Main(string[] args){    using (var context = new SchoolDbContext())
    {        var entityClass = new Class() { Name = "calss1" };        var entityStudent = new Student() { ClassId = entityClass.ClassId, Name = "student1" };
        context.Classs.Add(entityClass);
        context.Students.Add(entityStudent);
        context.SaveChanges();
    }
}

执行结果:

可以看到,Student 表中的 ClassId 值是 0,而并不是我们预想的 1,这是一个问题,在不增加外键的情况下,我们一般会这样解决:

static void Main(string[] args){    using (var context = new SchoolDbContext())
    {        var entityClass = new Class() { Name = "calss2" };
        context.Classs.Add(entityClass);
        context.SaveChanges();        var entityStudent = new Student() { ClassId = entityClass.ClassId, Name = "student2" };
        context.Students.Add(entityStudent);
        context.SaveChanges();
    }
}

执行结果:

这种处理方式,虽然“解决”上面的问题,但其实有很多的隐患,多执行一次 SaveChanges,EF 就会多发起一次请求,增加了性能开销,并且 SaveChanges 是事务性的,如果第一个执行成功了,第二个执行失败了,这时候第一个事务并不会回滚,因为它独立于第二个,所以,最后就会造成数据的不一致性,虽然几率非常点,但我们应该尽量避免。

那有没有更好的解决方式呢?这个问题我之前有点想复杂了,其实解决非常简单,就是在 Student 实体中添加 virtual 修饰的 Class 属性,就可以了,如下:

public class Student{    public int StudentId { get; set; }    public int ClassId { get; set; }    public string Name { get; set; }    public virtual Class Class { get; set; }//添加属性}

需要注意的是,我们并不需要在 OnModelCreating 中进行 Class 和 ClassId 的映射配置,EF 会自动查找 ClassId(属性名 + Id),所以,“外键”命名要注意规范统一,如果命名为 Class_Id 就无效了。

再次执行添加实体的代码,发现会报错:

什么意思呢?就是实体更改了,需要进行 EF 迁移,如果你进行 EF 迁移的话,会发现,虽然我们没有在 OnModelCreating 中进行 ClassId 外键映射配置,但 EF 也会自动映射 ForeignKey 到数据库的,所以代码命名尽量规范些,EF 是比较“智能”的。

我们解决这个问题的前提条件是“不增加外键配置”,所以我们要让 EF 忽略实体更改:

public SchoolDbContext()
    : base("db_school"){
    Database.SetInitializer<SchoolDbContext>(null);//忽略映射}

再次执行添加实体代码:

static void Main(string[] args){    using (var context = new SchoolDbContext())
    {        var entityClass = new Class() { Name = "calss3" };        var entityStudent = new Student() { ClassId = entityClass.ClassId, Name = "student3" };
        context.Classs.Add(entityClass);
        context.Students.Add(entityStudent);
        context.SaveChanges();
    }
}

执行结果:

Entity Framework 实体框架的形成之旅--实体框架的开发的几个经验总结

在前阵子,我对实体框架进行了一定的研究,然后把整个学习的过程开了一个系列,以逐步深入的方式解读实体框架的相关技术,期间每每碰到一些新的问题需要潜入研究。本文继续前面的主题介绍,着重从整体性的来总结一下实体框架的一些方面,希望针对这些实际问题,和大家进行学习交流。

我的整个实体框架的学习和研究,是以我的Winform框架顺利升级到这个实体框架基础上为一个阶段终结,这个阶段事情很多,从开始客运联网售票的WebAPI平台的开发,到微软实体框架的深入研究,以及《基于Metronic的Bootstrap开发框架经验总结》的主题学习和分享等等方面,都混到一起来了,多个主题之间穿插着写一些随笔,也是希望把自己的学习过程进行记录总结,不用等到最后全部忘记了。

1、实体框架主键的类型约束问题

在我们搭建整个实体框架的过程中,我们一般都是抽象封装处理很多基础的增删改查、分页等常见的数据处理功能,如下所示。

        /// <summary>
        /// 更新对象属性到数据库中        /// </summary>
        /// <param name="t">指定的对象</param>
        /// <param name="key">主键的值</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        bool Update(T t, object key);        /// <summary>
        /// 更新对象属性到数据库中(异步)        /// </summary>
        /// <param name="t">指定的对象</param>
        /// <param name="key">主键的值</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        Task<bool> UpdateAsync(T t, object key);        /// <summary>
        /// 根据指定对象的ID,从数据库中删除指定对象        /// </summary>
        /// <param name="id">对象的ID</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c>。</returns>
        bool Delete(object id);        /// <summary>
        /// 根据指定对象的ID,从数据库中删除指定对象(异步)        /// </summary>
        /// <param name="id">对象的ID</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c>。</returns>
        Task<bool> DeleteAsync(object id);        /// <summary>
        /// 查询数据库,返回指定ID的对象        /// </summary>
        /// <param name="id">ID主键的值</param>
        /// <returns>存在则返回指定的对象,否则返回Null</returns>
        T FindByID(object id);        /// <summary>
        /// 查询数据库,返回指定ID的对象(异步)        /// </summary>
        /// <param name="id">ID主键的值</param>
        /// <returns>存在则返回指定的对象,否则返回Null</returns>
        Task<T> FindByIDAsync(object id);

上面的外键统一定义为object类型,因为我们为了主键类型通用的考虑。

在实际上表的外键类型可能是很多种的,如可能是常见的字符类型,也可能是int类型,也可能是long类型等等。如果我们更新、查找、删除整形类型的记录的时候,那么可能机会出现错误:

The argument types 'Edm.Int32' and 'Edm.String' are incompatible for this operation.

这些错误就是主键类型不匹配导致的,我们操作这些接口的时候,一定要传入对应类型给它们,才能正常的处理。

本来想尝试在内部进行转换处理为正确的类型的,不过没有找到很好的解决方案来识别和处理,因此最好的解决方法,就是我们调用这些有object类型主键的接口时,传入正确的类型即可。

                    RoleInfo info = CallerFactory<IRoleService>.Instance.FindByID(currentID.ToInt32());                    if (info != null)
                    {
                        info = SetRoleInfo(info);
                        CallerFactory<IRoleService>.Instance.Update(info, info.ID);

                        RefreshTreeView();
                    }

又或者是下面的代码:

        /// <summary>
        /// 分页控件删除操作        /// </summary>
        private void winGridViewPager1_OnDeleteSelected(object sender, EventArgs e)
        {            if (MessageDxUtil.ShowYesNoAndTips("您确定删除选定的记录么?") == DialogResult.No)
            {                return;
            }            int[] rowSelected = this.winGridViewPager1.GridView1.GetSelectedRows();            foreach (int iRow in rowSelected)
            {                string ID = this.winGridViewPager1.GridView1.GetRowCellDisplayText(iRow, "ID");                CallerFactory<IDistrictService>.Instance.Delete(ID.ToInt64());
            }

            BindData();
        }

2、递归函数的处理

在很多时候,我们都会用到递归函数的处理,这样能够使得我们把整个列表的内容都合理的提取出来,是我们开发常见的知识点之一。

不过一般在处理LINQ的时候,它的递归函数的处理和我们普通的做法有一些差异。

例如我们如果要获取一个树形机构列表,如果我们指定了一个开始的机构节点ID,我们需要递归获取下面的所有层次的集合的时候,常规的做法如下所示。

        /// <summary>
        /// 根据指定机构节点ID,获取其下面所有机构列表        /// </summary>
        /// <param name="parentId">指定机构节点ID</param>
        /// <returns></returns>
        public List<OUInfo> GetAllOUsByParent(int parentId)
        {
            List<OUInfo> list = new List<OUInfo>();            string sql = string.Format("Select * From {0} Where Deleted <> 1 Order By PID, Name ", tableName);

            DataTable dt = SqlTable(sql);            string sort = string.Format("{0} {1}", GetSafeFileName(sortField), isDescending ? "DESC" : "ASC");
            DataRow[] dataRows = dt.Select(string.Format(" PID = {0}", parentId), sort);            for (int i = 0; i < dataRows.Length; i++)
            {                string id = dataRows[i]["ID"].ToString();
                list.AddRange(GetOU(id, dt));
            }            return list;
        }        private List<OUInfo> GetOU(string id, DataTable dt)
        {
            List<OUInfo> list = new List<OUInfo>();

            OUInfo ouInfo = this.FindByID(id);
            list.Add(ouInfo);            string sort = string.Format("{0} {1}", GetSafeFileName(sortField), isDescending ? "DESC" : "ASC");
            DataRow[] dChildRows = dt.Select(string.Format(" PID={0} ", id), sort);            for (int i = 0; i < dChildRows.Length; i++)
            {                string childId = dChildRows[i]["ID"].ToString();
                List<OUInfo> childList = GetOU(childId, dt);
                list.AddRange(childList);
            }            return list;
        }

这里面的大概思路就是把符合条件的集合全部弄到DataTable集合里面,然后再在里面进行检索,也就是递归获取里面的内容。

上面是常规的做法,可以看出代码量还是太多了,如果使用LINQ,就不需要这样了,而且也不能这样处理。

使用实体框架后,主要就是利用LINQ进行一些集合的操作,这些LINQ的操作虽然有点难度,不过学习清楚了,处理起来也是比较方便的。

在数据访问层,处理上面同等的功能,LINQ操作代码如下所示。

        /// <summary>
        /// 根据指定机构节点ID,获取其下面所有机构列表        /// </summary>
        /// <param name="parentId">指定机构节点ID</param>
        /// <returns></returns>
        public IList<Ou> GetAllOUsByParent(int parentId)
        {            //递归获取指定PID及下面所有所有的OU
            var query = this.GetQueryable().Where(s => s.PID == parentId).Where(s => !s.Deleted.HasValue || s.Deleted == 0).OrderBy(s => s.PID).OrderBy(s => s.Name);            return query.ToList().Concat(query.ToList().SelectMany(t => GetAllOUsByParent(t.ID))).ToList();
        }

基本上,可以看到就是两行代码了,是不是很神奇,它们实现的功能完全一致。

不过,也不是所有的LINQ递归函数都可以做的非常简化,有些递归函数,我们还是需要使用常规的思路进行处理。

        /// <summary>
        /// 获取树形结构的机构列表        /// </summary>
        public IList<OuNodeInfo> GetTree()
        {
            IList<OuNodeInfo> returnList = new List<OuNodeInfo>();
            IList<Ou> list = this.GetQueryable().Where(p => p.PID == -1).OrderBy(s => s.PID).OrderBy(s => s.Name).ToList();            if (list != null)
            {                foreach (Ou info in list.Where(s => s.PID == -1))
                {
                    OuNodeInfo nodeInfo = GetNode(info);
                    returnList.Add(nodeInfo);
                }
            }            return returnList;
        }

不过相对来说,LINQ已经给我们带来的非常大的便利了。

3、日期字段类型转换的错误处理

我们在做一些表的时候,一般情况下都会有日期类型存在,如我们的生日,创建、编辑日期等,一般我们数据库可能用的是datetime类型,如果这个日期的类型内容在下面这个区间的话:

"0001-01-01 到 9999-12-31"(公元元年 1 月 1 日到公元 9999 年 12 月 31 日)

我们可能就会得到下面的错误:

从 datetime2 数据类型到 datetime 数据类型的转换产生一个超出范围的值

一般之所以会报错数据类型转换产生一个超出范围的值,都是因为数据的大小和范围超出要转换的目标的原因。我们先看datetime2和datetime这两个数据类型的具体区别在哪里。

官方MSDN对于datetime2的说明:定义结合了 24 小时制时间的日期。 可将 datetime2 视作现有 datetime 类型的扩展,其数据范围更大,默认的小数精度更高,并具有可选的用户定义的精度。

这里值的注意的是datetime2的日期范围是"0001-01-01 到 9999-12-31"(公元元年 1 月 1 日到公元 9999 年 12 月 31 日)。而datetime的日期范围是:”1753 年 1 月 1 日到 9999 年 12 月 31 日“。这里的日期范围就是造成“从 datetime2 数据类型到 datetime 数据类型的转换产生一个超出范围的值”这个错误的原因!!!

在c#中,如果实体类的属性没有赋值,一般都会取默认值,比如int类型的默认值为0,string类型默认值为null, 那DateTime的默认值呢?由于DateTime的默认值为"0001-01-01",所以entity framework在进行数据库操作的时候,在传入数据的时会自动将原本是datetime类型的数据字段转换为datetime2类型(因为0001-01-01这个时间超出了数据库中datetime的最小日期范围),然后在进行数据库操作。问题来了,虽然EF已经把要保存的数据自动转为了datetime2类型,但是数据库中表的字段还是datetime类型!所以将datetime2类型的数据添加到数据库中datetime类型的字段里去,就会报错并提示转换超出范围。

解决方法如下所示:

这个问题的解决方法:

  1. C#代码中 DateTime类型的字段在作为参数传入到数据库前记得赋值,并且的日期要大于1753年1月1日。
  2. C#代码中 将原本是DateTime类型的字段修改为DateTime?类型,由于可空类型的默认值都是为null,所以传入数据库就可以不用赋值,数据库中的datetime类型也是支持null值的。
  3. 修改数据库中表的字段类型,将datetime类型修改为datetime2类型

例如,我在实体框架里面,对用户表的日期类型字段进行初始化,这样就能保证我存储数据的时候,默认值是不会有问题的。

    /// <summary>
    /// 系统用户信息,数据实体对象    /// </summary>
    public class User
    { 
        /// <summary>
        /// 默认构造函数(需要初始化属性的在此处理)        /// </summary>
        public User()
        {            this.ID= 0;            //从 datetime2 数据类型到 datetime 数据类型的转换产生一个超出范围的值            //避免这个问题,可以初始化日期字段
            DateTime defaultDate = Convert.ToDateTime("1900-1-1");            this.Birthday = defaultDate;            this.LastLoginTime = defaultDate;            this.LastPasswordTime = defaultDate;            this.CurrentLoginTime = defaultDate;            this.EditTime = DateTime.Now;            this.CreateTime = DateTime.Now;
         }

有时候,虽然这样设置了,但是在界面可能给这个日期字段设置了不合理的值,也可能产生问题。那么我们对于这种情况,判断一下,如果小于某个值,我们给它一个默认值。

4、实体框架的界面处理

在界面调整这块,我们还是尽可能保持着的Enterprise Library的Winform界面样式,也就是混合型或者普通Winform的界面效果。不过这里我们是以混合式框架进行整合测试,因此实体框架的各个方面的调用处理基本上保持一致。

不过由于实体框架里面,实体类避免耦合的原因,我们引入了DTO的概念,并使用了AutoMapper组件进行了Entity与DTO的相互映射,具体介绍可以参考《Entity Framework 实体框架的形成之旅--数据传输模型DTO和实体模型Entity的分离与联合

》。

因此我们在界面操作的都是DTO对象类型了,我们在定义的时候,为了避免更多的改动,依旧使用***Info这样的类名称作为DTO对象的名称,***代表表名对象。

在混合式框架的界面表现层,它们的数据对象的处理基本上保持和原来的代码差不多。

        /// <summary>
        /// 新增状态下的数据保存        /// </summary>
        /// <returns></returns>
        public override bool SaveAddNew()
        {            UserInfo info = tempInfo;//必须使用存在的局部变量,因为部分信息可能被附件使用            SetInfo(info);
            info.Creator = Portal.gc.UserInfo.FullName;
            info.Creator_ID = Portal.gc.UserInfo.ID.ToString();
            info.CreateTime = DateTime.Now;            try
            {                #region 新增数据                bool succeed = CallerFactory<IUserService>.Instance.Insert(info);                if (succeed)
                {                    //可添加其他关联操作

                    return true;
                }                #endregion
            }            catch (Exception ex)
            {
                LogTextHelper.Error(ex);
                MessageDxUtil.ShowError(ex.Message);
            }            return false;
        }

但我们需要在WCF服务层说明他们之间的映射关系,方便进行内部的转换处理。

在实体框架界面层的查询中,我们也不在使用部分SQL的条件做法了,采用更加安全的基于DTO的LINQ表达式进行封装,最后传递给后台的也就是一个LINQ对象(非传统方式的实体LINQ,那样在分布式处理中会出错)。

如查询条件的封装处理如下所示:

        /// <summary>
        /// 根据查询条件构造查询语句        /// </summary> 
        private ExpressionNode GetConditionSql()
        {
            Expression<Func<UserInfo, bool>> expression = p => true;            if (!string.IsNullOrEmpty(this.txtHandNo.Text))
            {
                expression = expression.And(x => x.HandNo.Equals(this.txtHandNo.Text));
            }            if (!string.IsNullOrEmpty(this.txtName.Text))
            {
                expression = expression.And(x => x.Name.Contains(this.txtName.Text));
            }
.........................................            //如果是公司管理员,增加公司标识
            if (Portal.gc.UserInRole(RoleInfo.CompanyAdminName))
            {
                expression = expression.And(x => x.Company_ID == Portal.gc.UserInfo.Company_ID);
            }            //如果是单击节点得到的条件,则使用树列表的,否则使用查询条件的
            if (treeCondition != null)
            {
                expression = treeCondition;
            }            //如非选定,只显示正常用户
            if (!this.chkIncludeDelete.Checked)
            {
                expression = expression.And(x => x.Deleted == 0);
            }            return expression.ToExpressionNode();
        }

而分页查询的处理,依旧和原来的风格差不多,只不过这里的Where条件为ExpressionNode 对象了,如代码所示、

            ExpressionNode where = GetConditionSql();
            PagerInfo PagerInfo = this.winGridViewPager1.PagerInfo;            IList<UserInfo> list = CallerFactory<IUserService>.Instance.FindWithPager(where, ref PagerInfo);            this.winGridViewPager1.DataSource = new WHC.Pager.WinControl.SortableBindingList<UserInfo>(list);            this.winGridViewPager1.PrintTitle = "系统用户信息报表";

最后我们来看看整个实体框架的结构和界面的效果介绍。

界面效果如下所示:

代码结构如下所示:

架构设计的效果图如下所示:

原文发布于微信公众号 - 我为Net狂(dotNetCrazy)

原文发表时间:2015-10-13

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏大内老A

WCF技术剖析之十四:泛型数据契约和集合数据契约(上篇)

在.NET Framework 2.0中,泛型第一次被引入。我们可以定义泛型接口、泛型类型、泛型委托和泛型方法。序列化依赖于真实具体的类型,而泛型则刻意模糊了具...

24680
来自专栏叁金大数据

自学Python十 爬虫实战三

  我又来送福利啦!!!不同于上篇文章,这次我们的爬虫采用了多线程,一直以来被所谓的分布式  多线程  爬虫 给唬的怕怕的。今天就来一发多线程爬虫吧,还能看妹子...

9110
来自专栏.NET后端开发

ADO.NET入门教程(六) 谈谈Command对象与数据检索

摘要 到目前为止,我相信大家对于ADO.NET如何与外部数据源建立连接以及如何提高连接性能等相关知识已经牢固于心了。连接对象作为ADO.NET的主力先锋,为用户...

43370
来自专栏我是业余自学C/C++的

redis_3.0.7_sds.c_sdsull2str()

18740
来自专栏逆向技术

异常处理第一讲(SEH),筛选器异常,以及__asm的扩展,寄存器注入简介

异常处理第一讲(SEH),筛选器异常,以及__asm的扩展 一丶__Asm的扩展知识 ①丶使用关键字,解决局部变量申请问题 昨天已经介绍了__asm的基本用法,...

320100
来自专栏菩提树下的杨过

byte[]数组下标的最大值

原以为int.MaxValue就是所有数组下标的最大值,编译也能通过,结果运行时发现报错:内存溢出。 经过测试发现,原来在不同的系统/不同的配置上,这个值都不太...

20450
来自专栏跟着阿笨一起玩NET

跨线程调用窗体控件

本文转载:http://www.csharpwin.com/csharpspace/11279r6763.shtml

33810
来自专栏Java大联盟

Java爬虫之匿名代理IP的获取

25130
来自专栏林德熙的博客

win10 uwp json

本文讲的是关于在uwp使用json的简单使用,json应用很多,因为我只是写简单使用,说的东西可能不对或者不符合每个人的预期。如果觉得我有讲的不对的,就多多包含...

12310
来自专栏大内老A

无需写try/catch,也能正常处理异常

对于企业应用的开发者来说,异常处理是一件既简单又复杂的事情。说其简单,是因为相关的编程无外乎try/catch/finally+throw而已;说其复杂,是因为...

29480

扫码关注云+社区

领取腾讯云代金券