欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

【ASP.NET Core】EF Core - “导航属性”

程序员文章站 2022-05-14 08:32:44
“导航属性”是实体框架用得算是比较频繁的概念。 首先,它是类型成员,其次,他是属性,这不是 F 话,而是明确它的本质。那么,什么场景下会用到导航属性呢?重点就落在“导航”一词上了,当实体 A 需要引用实体 B 时,实体 A 中需要公开一个属性,通过这个属性,能找到关联的实体 B。 又或者,X 实体表 ......

“导航属性”是实体框架用得算是比较频繁的概念。

首先,它是类型成员,其次,他是属性,这不是 f 话,而是明确它的本质。那么,什么场景下会用到导航属性呢?重点就落在“导航”一词上了,当实体 a 需要引用实体 b 时,实体 a 中需要公开一个属性,通过这个属性,能找到关联的实体 b。

又或者,x 实体表示你的博客,p 实体表示你发的一篇博文。你的博客肯定会发很多博文的,所以,x 实体中可能需要一个 list<p> 类型的属性,这个属性包含了你的博客所发表的文章。通过一个实体的属性成员,可以定位到与之有关联的实体,这就是导航的用途了。就像你开着车去穿越神农架一样,迷路了就打开高德导航(前提是不存在定位干扰)。

现在跑江湖的人多,通过各种江湖骗术发家致富。有了不正常的财富积累后,他们开始大量买车,还买地打造个人车库。于是,person 实体代表这些有钱人,cardata 实体表示他们买的各种壕车。

    public class person
    {
        public int pid { get; set; }
        public string name { get; set; }
        public int age { get; set; }
        public list<cardata> cars { get; set; }
    }

    public class cardata
    {
        public guid carid { get; set; }
        public string carattribute { get; set; }
        public decimal cost { get; set; }
    }

每个 person 都有 cars 属性,表示各自所购买的车。这个 cars 就是导航属性,通过这个属性能找到关联的 cardata 实体。

再定义一个数据上下文类。

    public class mycontext : dbcontext
    {
        public dbset<person> persons
        {
            get { return set<person>(); }
        }
    }

公开一个 persons 属性,便于访问,当然了,你觉得我那样写代码太多,你可以直接来这样。

        public dbset<person> persons { get; set; }

两种写法都是可以的。

这一次,我选择用 sqlite 数据库,新的 .net core 框架没有包含访问 sqlite 的程序集,不过没关系,有 nuget 啥都能裹进来。怎么安装 nuget 包就不用我教了,你会的。最简单不粗暴的方法就是直接在 nuget 控制台中执行 install-package 命令。

pm> install-package microsoft.entityframeworkcore.sqlite

看到下面这一堆东东就说明完成了。

【ASP.NET Core】EF Core -  “导航属性”

 

回到 mycontext 类,进行一下连接字符串的配置。

        protected override void onconfiguring(dbcontextoptionsbuilder optionsbuilder)
        {
            optionsbuilder.usesqlite("data source=tmd.db");
        }

重写 onconfiguring 方法,再调用 usesqlite 扩展方法,就可以设置连接字符串了。

还要重写 onmodelcreating 方法,要做两件事情:一是为每个实体设置主键;二是为两个实体建立关系。

        protected override void onmodelcreating(modelbuilder modelbuilder)
        {
            // 设置主键
            modelbuilder.entity<person>().haskey(p => p.pid);
            modelbuilder.entity<cardata>().haskey(c => c.carid);
            // 映射实体关系,一对多
            modelbuilder.entity<person>().hasmany(p => p.cars);
        }

在本例中,你懂的,一个人可以有 n 辆车,因此 person 与 cardata 之间是“一对多”的关,故而实体 person 可以 hasmany 个 cardata 对象,其中,cars 即是导航属性。

 

注意:由于 mycontext 类重写了 onconfiguring 方法,所以,在 mycontext 类的构造函数中,无需接收 dbcontextoptions<mycontext> 的依赖注入 ,在 startup.configureservices 方法中也无需再调用 usesqlite 方法,你只需 add 一下就可以了。

        public void configureservices(iservicecollection services)
        {
            services.adddbcontext<mycontext>();
            services.addmvc();
        }

 在 main 入口点中,先创建 host 实例。

            var host = new webhostbuilder()
                .usekestrel()
                .usecontentroot(directory.getcurrentdirectory())
                .useenvironment(environmentname.development)
                .useurls("http://localhost:7552")
                .usestartup<startup>()
                .build();

此时,不要急着调用 run 方法。因为咱们还没创建数据库呢。当然,你可以用老周上一篇中介绍的方法,在 nuget 控制台中,用 add-migration 命令添加迁移,然后用 update-database 命令创建数据库。不过,本文中,老周将通过代码在运行阶段创建数据库。

            using (iservicescope scope = host.services.createscope())
            {
                mycontext cxt = scope.serviceprovider.getrequiredservice<mycontext>();
                if (cxt.database.ensurecreated())
                {
                    // 插入一些记录
                    person p1 = new person
                    {
                        name = "王老三",
                        age = 65,
                        cars = new list<cardata>
                        {
                            new cardata
                            {
                                carattribute= "黄色兰博基尼",
                                cost = 1500020002.00m
                            },
                            new cardata
                            {
                                carattribute = "景泰蓝 吉利瑞博ge",
                                cost = 138_000m
                            }
                        }
                    };
                    cxt.persons.add(p1);
                    person p2 = new person
                    {
                        name = "朱大日",
                        age = 72,
                        cars = new list<cardata>
                        {
                            new cardata
                            {
                                carattribute = "玛瑙红 别克velite 5",
                                cost = 289_500m
                            },
                            new cardata
                            {
                                carattribute = "雅韵金 本田inspire",
                                cost = 171000m
                            },
                           new cardata
                           {
                               carattribute = "奥迪a4l",
                               cost = 401000m
                           }
                        }
                    };
                    cxt.persons.add(p2);
                    // 更新到数据库
                    cxt.savechanges();
                }
            }

iservicescope 是个有趣的玩意儿,它创建一个基于当前作用域的服务列表,从该对象中获取的服务实例,其生命周期只在当前 scope 中有效。这特别适用于临时实例化服务的情景。比如这里,mycontext 只是暂时实例化,等创建数据库并写入测试数据后,就可以 dispose 了。

初始化数据库后,可以运行 host 了。 

        host.run();

 

添加一个控制器,为了简单,咱们不创建 view 了,就直接返回 json 数据好了,就当 web api 来使用。

    [route("[controller]/[action]")]
    public class testcontroller : controller
    {
        readonly mycontext context;
        public testcontroller(mycontext c)
        {
            context = c;
        }

        [httpget]
        public actionresult listdata()
        {
            return json(context.persons);
        }
    }

 

现在可以运行了,用诸如 postman 等测试工具,请求 <root url>/test/listdata,结果发现惊人一幕。

[
    {
        "pid": 1,
        "name": "王老三",
        "age": 65,
        "cars": null
    },
    {
        "pid": 2,
        "name": "朱大日",
        "age": 72,
        "cars": null
    }
]

我相信,很多人都遇到了这个问题,所以,本文老周也顺便解释一下这个问题。如你所见,cars 属性是 null,明明是添加了 cardata 对象的,为啥会 null,你是不是开始怀疑人生了?千万不要轻易怀疑人生,那样是很不负责任的。

好,不卖关子了。出现这个问题,是因为导航属性的状态在默认情况下不会自动去还原的,不然的话,会增加对象引用,所以默认是不加载的。那么,你会问,那么 cardata 实体的数据记录到底加载了没?加载了的,你可以写一个 action 去试试的。

        [httpget]
        public actionresult carlist()
        {
            var cars = context.set<cardata>().tolist();
            return json(cars);
        }

然后,你访问一下 <root url>/test/carlist,看看下面的结果。

[
    {
        "carid": "36e97ed0-56b1-4d92-bb2d-aeec9f9e1b43",
        "carattribute": "黄色兰博基尼",
        "cost": 1500020002
    },
    {
        "carid": "0fd6c2a0-d4ef-4838-bc08-43a5cb024eef",
        "carattribute": "景泰蓝 吉利瑞博ge",
        "cost": 138000
    },
    {
        "carid": "c9eb20c8-931e-4563-b380-cbee926015c8",
        "carattribute": "玛瑙红 别克velite 5",
        "cost": 289500
    },
    {
        "carid": "3d563693-5ae0-4682-bd53-c7fc87e951de",
        "carattribute": "雅韵金 本田inspire",
        "cost": 171000
    },
    {
        "carid": "2294a556-fd02-49c3-b4b2-559f15413e75",
        "carattribute": "奥迪a4l",
        "cost": 401000
    }
]

我没骗你吧,有数据的呀。

现在我们有这个需求,要求还原导航属性的状态,那咋办呢?再次回到 listdata 方法,把它改成这样。

        [httpget]
        public actionresult listdata()
        {
            var persons = context.persons.include(p => p.cars).tolist();
            return json(persons);
        }

调用 include 方法记得引入 microsoft.entityframeworkcore 命名空间,这个不用我多说了。incluse 扩展方法的意思就是加载导航属性中的内容,它会自动还原状态,知道哪些 cardata 实例与 person 实例有关。

再次运行,请求一下 <root url>/test/listdata,这下你就放心了。

[
    {
        "pid": 1,
        "name": "王老三",
        "age": 65,
        "cars": [
            {
                "carid": "36e97ed0-56b1-4d92-bb2d-aeec9f9e1b43",
                "carattribute": "黄色兰博基尼",
                "cost": 1500020002
            },
            {
                "carid": "0fd6c2a0-d4ef-4838-bc08-43a5cb024eef",
                "carattribute": "景泰蓝 吉利瑞博ge",
                "cost": 138000
            }
        ]
    },
    {
        "pid": 2,
        "name": "朱大日",
        "age": 72,
        "cars": [
            {
                "carid": "c9eb20c8-931e-4563-b380-cbee926015c8",
                "carattribute": "玛瑙红 别克velite 5",
                "cost": 289500
            },
            {
                "carid": "3d563693-5ae0-4682-bd53-c7fc87e951de",
                "carattribute": "雅韵金 本田inspire",
                "cost": 171000
            },
            {
                "carid": "2294a556-fd02-49c3-b4b2-559f15413e75",
                "carattribute": "奥迪a4l",
                "cost": 401000
            }
        ]
    }
]

怎么样,满意了吧。

 

接下来,咱们再看看反过来的情况,咋返过来呢?我们假设以汽车为主,现在是每辆车都对应着一位车主信息,每个人只与一辆车关联,所以,车与人之间是“一对一”的关系。

先定义实体类,结构与前面的差不多。

    public class person
    {
        public int pid { get; set; }
        public string name { get; set; }
    }

    public class cardata
    {
        public int carid { get; set; }
        public string carattribute { get; set; }
        public decimal cost { get; set; }
        public person owner { get; set; }
    }

这一次,如你所见,导航属性是 cardata 类的 owner 属性,即该车的车主信息,引用一个 person 实例。

下面定义 dbcontext。

    public class mycontext : dbcontext
    {
        public dbset<person> persons { get; set; }
        public dbset<cardata> cars { get; set; }
    }

重写 onmodelcreating 方法。

        protected override void onmodelcreating(modelbuilder modelbuilder)
        {
            modelbuilder.entity<person>().haskey(p => p.pid);
            modelbuilder.entity<cardata>().haskey(c => c.carid);

            modelbuilder.entity<cardata>().hasone(c => c.owner);
        }

除了每两个实体设置主键外,请注意看最后一行,这一次,cardata 实体只对应着一个 person 实例,所以是 hasone,导航属性是 owner。

重写 onconfiguring 方法,配置连接字符串,这一次就用 sql server localdb,轻量级的。

        protected override void onconfiguring(dbcontextoptionsbuilder optionsbuilder)
        {
            optionsbuilder.usesqlserver("server=(localdb)\\mssqllocaldb;database=demodt");
        }

main 方法中的做法与前面一样,初始化 webhost 后,先创建数据库,填一下垃圾数据,然后再启动 host。

            var host = new webhostbuilder()
                .usekestrel()
                .useenvironment("debug")
                .useurls("http://localhost:19230")
                .usestartup<startup>()
                .usecontentroot(directory.getcurrentdirectory())
                .build();

            using(iservicescope scope = host.services.createscope())
            {
                mycontext cxt = scope.serviceprovider.getrequiredservice<mycontext>();
                if (cxt.database.ensurecreated())
                {
                    person p1 = new person
                    {
                        name = "王阿基"
                    };
                    person p2 = new person
                    {
                        name = "刘二打"
                    };
                    person p3 = new person
                    {
                        name = "李无牙"
                    };
                    cardata c1 = new cardata
                    {
                        carattribute = "三无产品 a款",
                        cost = 150000m,
                        owner = p1
                    };
                    cardata c2 = new cardata
                    {
                        carattribute = "三无产品 f款",
                        cost = 67500m,
                        owner = p2
                    };
                    cardata c3 = new cardata
                    {
                        carattribute = "三无产品 2018款",
                        cost = 76000m,
                        owner = p3
                    };
                    cxt.persons.add(p1);
                    cxt.persons.add(p2);
                    cxt.persons.add(p3);
                    cxt.cars.add(c1);
                    cxt.cars.add(c2);
                    cxt.cars.add(c3);
                    cxt.savechanges();
                }
            }

            host.run();

接下来,创建一个控制器。

    public class samplecontroller : controller
    {
    }  

通过依赖注入,获得 mycontext 实例。

        readonly mycontext context;
        public samplecontroller(mycontext cxt)
        {
            context = cxt;
        }

定义一个获取数据列表的 action。

        [httpget]
        public actionresult list()
        {
            var cars = context.cars.include(c => c.owner);
            return json(cars.tolist());
        }

include 方法的调用与前面一样,只是注意这次是以 cardata 实体为主,顺便加载导航属性 owner 的内容。

postman 测试结果。

[
    {
        "carid": 1,
        "carattribute": "三无产品 a款",
        "cost": 150000,
        "owner": {
            "pid": 1,
            "name": "王阿基"
        }
    },
    {
        "carid": 2,
        "carattribute": "三无产品 f款",
        "cost": 67500,
        "owner": {
            "pid": 2,
            "name": "刘二打"
        }
    },
    {
        "carid": 3,
        "carattribute": "三无产品 2018款",
        "cost": 76000,
        "owner": {
            "pid": 3,
            "name": "李无牙"
        }
    }
]

同样,这也达到预期的效果了。

 

我们查看一下所生成的数据库,你会发现,cars 表中生成了一列,名为 ownerpid,引用的是关联的 person 实例的主键。加载数据时,就是通过这一列来还原两个实体之间的关系的。

【ASP.NET Core】EF Core -  “导航属性”