单向1 - *关联(可为空)

📅 2026/7/3 1:13:13
单向1 - *关联(可为空)
这里新登场角色是和发票发票有自己的编号有些产品有发票有些产品没有发票。我们希望通过产品找到发票而又不需要由发票关联到产品。123456publicclassInvoice{publicintId {get;set; }publicstringInvoiceNo {get;set; }publicDateTime CreateDate {get;set; }}产品类新增的属性如下12publicvirtualInvoice Invoice {get;set; }publicint? InvoiceId {get;set; }可以使用如下代码创建Product到Invoice的关联123456789101112131415161718publicclassProductMap : EntityTypeConfigurationProduct{publicProductMap(){ToTable(Product);HasKey(p p.Id);HasOptional(p p.Invoice).WithMany().HasForeignKey(p p.InvoiceId);}}publicclassInvoiceMap : EntityTypeConfigurationInvoice{publicInvoiceMap(){ToTable(Invoice);HasKey(i i.Id);}}HasOptional表示一个产品可能会有发票WithMany的参数为空表示我们不需要由发票关联到产品HasForeignKey用来指定Product表中的外键列。还可以通过WillCascadeOnDelete()配置是否级联删除这个大家都知道就不多说了。运行迁移后数据库生成的Product表外键可为空注意实体类中表示外键的属性一定要为Nullable类型不然迁移代码不能生成。下面写段代码来测试下这个映射配置先是创建一个测试对象123456789101112varproduct newProduct(){Name 书,Description 码农书籍,Invoice newInvoice()//这里不创建Invoice也可以因为其可以为null{InvoiceNo 12345,CreateDate DateTime.Now}};context.SetProduct().Add(product);context.SaveChanges();然后查询注意创建和查询要分2次执行不然不会走数据库直接由EF Context返回结果了。1varproductGet context.SetProduct().Include(pp.Invoice).FirstOrDefault();通过SS Profiler可以看到生成的SQL如下12345678910SELECTTOP(1)[Extent1].[Id]AS[Id],[Extent1].[Name]AS[Name],[Extent1].[Description]AS[Description],[Extent1].[InvoiceId]AS[InvoiceId],[Extent2].[Id]AS[Id1],[Extent2].[InvoiceNo]AS[InvoiceNo],[Extent2].[CreateDate]AS[CreateDate]FROM[dbo].[Products]AS[Extent1]LEFTOUTERJOIN[dbo].[Invoices]AS[Extent2]ON[Extent1].[InvoiceId] [Extent2].[Id]可以看到对于外键可空的情况EF生成的SQL使用了LEFT OUTER JOIN基本上复合我们的期待。单向1 - *关联不可为空为了演示这个关联请出一个新对象合格证合格证有自己的编号而且一个产品是必须有合格证。12345publicclassCertification{publicintId {get;set; }publicstringInspector {get;set; }}我们给Product添加关联合格证的属性12publicvirtualCertification Certification {get;set; }publicintCertificationId {get;set; }配置Product到Certification映射的代码与之前的类似就是把HasOptional换成了HasRequired1HasRequired(p p.Certification).WithMany().HasForeignKey(pp.CertificationId);生成的迁移代码外键列不能为空。创建对象时Product必须和Certification一起创建。生成的查询语句除了把LEFT OUTER JOIN换成INNER JOIN外其他都一样不再赘述。双向1 - *关联这是比较常见的场景如一个产品可以对应多张照片每张照片关联一个产品。先来看看新增的照片类12345678publicclassProductPhoto{publicintId {get;set; }publicstringFileName {get;set; }publicfloatFileSize {get;set; }publicvirtualProduct Product {get;set; }publicintProductId {get;set; }}给Product增加ProductPhoto集合1publicvirtualICollectionProductPhoto Photos {get;set; }然后是映射配置123456789101112131415161718publicclassProductMap : EntityTypeConfigurationProduct{publicProductMap(){ToTable(Product);HasKey(p p.Id);HasMany(p p.Photos).WithRequired(pp pp.Product).HasForeignKey(pp pp.ProductId);}}publicclassProductPhotoMap : EntityTypeConfigurationProductPhoto{publicProductPhotoMap(){ToTable(ProductPhoto);HasKey(pp pp.Id);}}代码很容易理解HasMany表示Product中有多个ProductPhotoWithRequired表示ProductPhoto一定会关联到一个Product。我们来看另一种等价的写法在ProductPhoto中配置关联123456789101112131415161718publicclassProductMap : EntityTypeConfigurationProduct{publicProductMap(){ToTable(Product);HasKey(p p.Id);}}publicclassProductPhotoMap : EntityTypeConfigurationProductPhoto{publicProductPhotoMap(){ToTable(ProductPhoto);HasKey(pp pp.Id);HasRequired(pp pp.Product).WithMany(p p.Photos).HasForeignKey(pp pp.ProductId);}}有没有感觉和之前单向1 - *的配置很像其实就是WithMany多了参数而已。随着例子越来越多大家应该对这几个配置理解的越来越深了。迁移到数据库后我们添加些数据测试下12345678910111213141516171819202122232425varproduct newProduct(){Name 投影仪,Description 高分辨率};context.SetProduct().Add(product);context.SaveChanges();ProductPhoto pp1 newProductPhoto(){FileName 正面图,FileSize 3,ProductId product.Id};ProductPhoto pp2 newProductPhoto(){FileName 侧面图,FileSize 5,ProductId product.Id};context.SetProductPhoto().Add(pp1);context.SetProductPhoto().Add(pp2);context.SaveChanges();试一试一次读取Product及ProductPhoto1varproductGet context.SetProduct().Include(pp.Photos).ToList();生成的SQL如下123456789101112SELECT[Limit1].[Id]AS[Id],[Limit1].[Name]AS[Name],[Limit1].[Description]AS[Description],[Extent2].[Id]AS[Id1],[Extent2].[FileName]AS[FileName],[Extent2].[FileSize]AS[FileSize],[Extent2].[ProductId]AS[ProductId],CASEWHEN([Extent2].[Id]ISNULL)THENCAST(NULLASint)ELSE1ENDAS[C1]FROM(SELECTTOP(1) [c].[Id]AS[Id], [c].[Name]AS[Name], [c].[Description]AS[Description]FROM[dbo].[Product]AS[c] )AS[Limit1]LEFTOUTERJOIN[dbo].[ProductPhoto]AS[Extent2]ON[Limit1].[Id] [Extent2].[ProductId]有点小复杂用LEFT OUTER JOIN的原因是可能有的Product没有ProductPhoto。* - *关联这次轮到产品标签登场了。一个产品可以有多个标签一个标签也可对应多个产品123456publicclassTag{publicintId {get;set; }publicstringText {get;set; }publicvirtualICollectionProduct Products {get;set; }}给Product增加标签集合1publicvirtualICollectionTag Tags {get;set; }映射代码123456789101112131415161718publicclassProductMap : EntityTypeConfigurationProduct{publicProductMap(){ToTable(Product);HasKey(p p.Id);HasMany(p p.Tags).WithMany(t t.Products).Map(m m.ToTable(Product_Tag_Mapping));}}publicclassTagMap : EntityTypeConfigurationTag{publicTagMap(){ToTable(Tag);HasKey(t t.Id);}}比较特殊的就是需要指定一个关联表保存多对多的映射关系。123456789101112CreateTable(dbo.Product_Tag_Mapping,c new{Product_Id c.Int(nullable:false),Tag_Id c.Int(nullable:false),}).PrimaryKey(t new{ t.Product_Id, t.Tag_Id }).ForeignKey(dbo.Product, t t.Product_Id, cascadeDelete:true).ForeignKey(dbo.Tag, t t.Tag_Id, cascadeDelete:true).Index(t t.Product_Id).Index(t t.Tag_Id);一般情况下使用自动生成的外键就好也可以自己定义外键名称。123456HasMany(p p.Tags).WithMany(t t.Products).Map(m {m.ToTable(Product_Tag_Mapping);m.MapLeftKey(Pid);m.MapRightKey(Tid);});迁移代码变成如下123456789101112CreateTable(dbo.Product_Tag_Mapping,c new{Pid c.Int(nullable:false),Tid c.Int(nullable:false),}).PrimaryKey(t new{ t.Pid, t.Tid }).ForeignKey(dbo.Product, t t.Pid, cascadeDelete:true).ForeignKey(dbo.Tag, t t.Tid, cascadeDelete:true).Index(t t.Pid).Index(t t.Tid);把映射代码中的WithMany参数去掉就是一种单向* - *的映射效果。如我们需要通过Product找到所有Tag但不需要通过Tag找到有这个标签的Product。有点类似与单向1 - *。但这里不管WithMany是否有参数生成的迁移代码都是一样的。我们也写点数据进去测试下123456789101112varproduct newProduct(){Name 投影仪,Description 高分辨率,Tags newListTag{newTag(){Text 性价比高}}};context.SetProduct().Add(product);context.SaveChanges();使用预加载(Include(pp.Tags))时的SQL123456789101112131415161718192021SELECT[Project1].[Id]AS[Id],[Project1].[Name]AS[Name],[Project1].[Description]AS[Description],[Project1].[C1]AS[C1],[Project1].[Id1]AS[Id1],[Project1].[Text]AS[Text]FROM(SELECT[Limit1].[Id]AS[Id],[Limit1].[Name]AS[Name],[Limit1].[Description]AS[Description],[Join1].[Id]AS[Id1],[Join1].[Text]AS[Text],CASEWHEN([Join1].[Product_Id]ISNULL)THENCAST(NULLASint)ELSE1ENDAS[C1]FROM(SELECTTOP(1) [c].[Id]AS[Id], [c].[Name]AS[Name], [c].[Description]AS[Description]FROM[dbo].[Product]AS[c] )AS[Limit1]LEFTOUTERJOIN(SELECT[Extent2].[Product_Id]AS[Product_Id], [Extent3].[Id]AS[Id], [Extent3].[Text]AS[Text]FROM[dbo].[Product_Tag_Mapping]AS[Extent2]INNERJOIN[dbo].[Tag]AS[Extent3]ON[Extent3].[Id] [Extent2].[Tag_Id] )AS[Join1]ON[Limit1].[Id] [Join1].[Product_Id])AS[Project1]ORDERBY[Project1].[Id]ASC, [Project1].[C1]ASC