news 2026/7/3 1:13:09

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
单向1 - *关联(可为空)

这里新登场角色是和发票发票有自己的编号,有些产品有发票,有些产品没有发票。我们希望通过产品找到发票而又不需要由发票关联到产品。

1

2

3

4

5

6

publicclassInvoice

{

publicintId {get;set; }

publicstringInvoiceNo {get;set; }

publicDateTime CreateDate {get;set; }

}

产品类新增的属性如下:

1

2

publicvirtualInvoice Invoice {get;set; }

publicint? InvoiceId {get;set; }

可以使用如下代码创建Product到Invoice的关联

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

publicclassProductMap : EntityTypeConfiguration<Product>

{

publicProductMap()

{

ToTable("Product");

HasKey(p => p.Id);

HasOptional(p => p.Invoice).WithMany().HasForeignKey(p => p.InvoiceId);

}

}

publicclassInvoiceMap : EntityTypeConfiguration<Invoice>

{

publicInvoiceMap()

{

ToTable("Invoice");

HasKey(i => i.Id);

}

}

HasOptional表示一个产品可能会有发票,WithMany的参数为空表示我们不需要由发票关联到产品,HasForeignKey用来指定Product表中的外键列。

还可以通过WillCascadeOnDelete()配置是否级联删除,这个大家都知道,就不多说了。

运行迁移后,数据库生成的Product表外键可为空(注意实体类中表示外键的属性一定要为Nullable类型,不然迁移代码不能生成)。

下面写段代码来测试下这个映射配置,先是创建一个测试对象

1

2

3

4

5

6

7

8

9

10

11

12

varproduct =newProduct()

{

Name ="书",

Description ="码农书籍",

Invoice =newInvoice()//这里不创建Invoice也可以,因为其可以为null

{

InvoiceNo ="12345",

CreateDate = DateTime.Now

}

};

context.Set<Product>().Add(product);

context.SaveChanges();

然后查询,注意,创建和查询要分2次执行,不然不会走数据库,直接由EF Context返回结果了。

1

varproductGet = context.Set<Product>().Include(p=>p.Invoice).FirstOrDefault();

通过SS Profiler可以看到生成的SQL如下:

1

2

3

4

5

6

7

8

9

10

SELECTTOP(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 - *关联(不可为空)

为了演示这个关联,请出一个新对象合格证合格证有自己的编号,而且一个产品是必须有合格证。

1

2

3

4

5

publicclassCertification

{

publicintId {get;set; }

publicstringInspector {get;set; }

}

我们给Product添加关联合格证的属性:

1

2

publicvirtualCertification Certification {get;set; }

publicintCertificationId {get;set; }

配置Product到Certification映射的代码与之前的类似,就是把HasOptional换成了HasRequired:

1

HasRequired(p => p.Certification).WithMany().HasForeignKey(p=>p.CertificationId);

生成的迁移代码,外键列不能为空。创建对象时Product必须和Certification一起创建。生成的查询语句除了把LEFT OUTER JOIN换成INNER JOIN外其他都一样,不再赘述。

双向1 - *关联

这是比较常见的场景,如一个产品可以对应多张照片,每张照片关联一个产品。先来看看新增的照片类

1

2

3

4

5

6

7

8

publicclassProductPhoto

{

publicintId {get;set; }

publicstringFileName {get;set; }

publicfloatFileSize {get;set; }

publicvirtualProduct Product {get;set; }

publicintProductId {get;set; }

}

给Product增加ProductPhoto集合:

1

publicvirtualICollection<ProductPhoto> Photos {get;set; }

然后是映射配置:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

publicclassProductMap : EntityTypeConfiguration<Product>

{

publicProductMap()

{

ToTable("Product");

HasKey(p => p.Id);

HasMany(p => p.Photos).WithRequired(pp => pp.Product).HasForeignKey(pp => pp.ProductId);

}

}

publicclassProductPhotoMap : EntityTypeConfiguration<ProductPhoto>

{

publicProductPhotoMap()

{

ToTable("ProductPhoto");

HasKey(pp => pp.Id);

}

}

代码很容易理解,HasMany表示Product中有多个ProductPhoto,WithRequired表示ProductPhoto一定会关联到一个Product。

我们来看另一种等价的写法(在ProductPhoto中配置关联):

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

publicclassProductMap : EntityTypeConfiguration<Product>

{

publicProductMap()

{

ToTable("Product");

HasKey(p => p.Id);

}

}

publicclassProductPhotoMap : EntityTypeConfiguration<ProductPhoto>

{

publicProductPhotoMap()

{

ToTable("ProductPhoto");

HasKey(pp => pp.Id);

HasRequired(pp => pp.Product).WithMany(p => p.Photos).HasForeignKey(pp => pp.ProductId);

}

}

有没有感觉和之前单向1 - *的配置很像?其实就是WithMany多了参数而已。随着例子越来越多,大家应该对这几个配置理解的越来越深了。

迁移到数据库后,我们添加些数据测试下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

varproduct =newProduct()

{

Name ="投影仪",

Description ="高分辨率"

};

context.Set<Product>().Add(product);

context.SaveChanges();

ProductPhoto pp1 =newProductPhoto()

{

FileName ="正面图",

FileSize = 3,

ProductId = product.Id

};

ProductPhoto pp2 =newProductPhoto()

{

FileName ="侧面图",

FileSize = 5,

ProductId = product.Id

};

context.Set<ProductPhoto>().Add(pp1);

context.Set<ProductPhoto>().Add(pp2);

context.SaveChanges();

试一试一次读取Product及ProductPhoto:

1

varproductGet = context.Set<Product>().Include(p=>p.Photos).ToList();

生成的SQL如下:

1

2

3

4

5

6

7

8

9

10

11

12

SELECT

[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。

* - *关联

这次轮到产品标签登场了。一个产品可以有多个标签,一个标签也可对应多个产品:

1

2

3

4

5

6

publicclassTag

{

publicintId {get;set; }

publicstringText {get;set; }

publicvirtualICollection<Product> Products {get;set; }

}

给Product增加标签集合:

1

publicvirtualICollection<Tag> Tags {get;set; }

映射代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

publicclassProductMap : EntityTypeConfiguration<Product>

{

publicProductMap()

{

ToTable("Product");

HasKey(p => p.Id);

HasMany(p => p.Tags).WithMany(t => t.Products).Map(m => m.ToTable("Product_Tag_Mapping"));

}

}

publicclassTagMap : EntityTypeConfiguration<Tag>

{

publicTagMap()

{

ToTable("Tag");

HasKey(t => t.Id);

}

}

比较特殊的就是需要指定一个关联表保存多对多的映射关系。

1

2

3

4

5

6

7

8

9

10

11

12

CreateTable(

"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);

一般情况下使用自动生成的外键就好,也可以自己定义外键名称。

1

2

3

4

5

6

HasMany(p => p.Tags).WithMany(t => t.Products).Map(m =>

{

m.ToTable("Product_Tag_Mapping");

m.MapLeftKey("Pid");

m.MapRightKey("Tid");

});

迁移代码变成如下:

1

2

3

4

5

6

7

8

9

10

11

12

CreateTable(

"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是否有参数,生成的迁移代码都是一样的。

我们也写点数据进去,测试下:

1

2

3

4

5

6

7

8

9

10

11

12

varproduct =newProduct()

{

Name ="投影仪",

Description ="高分辨率",

Tags =newList<Tag>

{

newTag(){Text ="性价比高"}

}

};

context.Set<Product>().Add(product);

context.SaveChanges();

使用预加载(Include(p=>p.Tags))时的SQL:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

SELECT

[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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/3 1:13:05

Hawkeye 0135-3987G电源模块

Hawkeye 0135-3987G 电源模块产品特点Hawkeye 0135-3987G 是 Hawkeye 生产的一款电源模块&#xff0c;主要用于为特定工业设备或子系统提供稳定的直流供电&#xff0c;确保设备持续可靠运行。该型号主要产品特点&#xff1a;为工业设备或特定系统提供稳定的直流供电。输入电压适…

作者头像 李华
网站建设 2026/7/3 1:08:11

7个节点串成Agent管道,6个场景全过,但和线上的差距都在细节里

今天干了一件事&#xff1a;把之前6天写的模块——安全检查、模型路由、缓存、上下文管理、LLM调用、输出审查、Token追踪——用责任链模式串成一条完整的Agent ChatBot管道。 7个节点&#xff0c;6个测试场景&#xff0c;全部跑通。正常对话能记住上下文&#xff0c;攻击输入0…

作者头像 李华
网站建设 2026/7/3 1:03:34

Multimodal-CoT:多模态思维链的工程落地与工业实践

1. 什么是真正的“思维链”——不是技巧&#xff0c;而是认知建模的底层迁移你有没有试过让ChatGPT解一道初中物理题&#xff0c;比如“一个质量为2kg的物体从10米高处自由下落&#xff0c;忽略空气阻力&#xff0c;求落地时的速度&#xff1f;”——它大概率会直接套用公式 $v…

作者头像 李华
网站建设 2026/7/3 1:02:11

求职简历怎么做才专业又好看?两个工具各自解决了一个方向的问题

求职简历怎么做才专业又好看&#xff1f;两个工具各自解决了一个方向的问题为什么推荐用专业的在线工具做简历一个人打开Word从零开始做简历&#xff0c;和打开一个专业平台做简历&#xff0c;花的时间可能差了三倍。功能再全的文字处理软件也不是为简历场景设计的——没有内容…

作者头像 李华
网站建设 2026/7/3 0:51:05

QQScreenShot深度解析:从逆向工程到高效截图工具的完整指南

QQScreenShot深度解析&#xff1a;从逆向工程到高效截图工具的完整指南 【免费下载链接】QQScreenShot 电脑QQ截图工具提取版,支持文字提取、图片识别、截长图、qq录屏。默认截图文件名为ScreenShot日期 项目地址: https://gitcode.com/gh_mirrors/qq/QQScreenShot QQSc…

作者头像 李华
网站建设 2026/7/3 0:44:33

AI Berkshire:多Agent协作的价值投资框架,让AI成为你的专业投研团队

如果你问 ChatGPT 或 Claude “拼多多值不值得买”&#xff0c;大概率会得到一篇“一方面增长强劲&#xff0c;另一方面竞争激烈&#xff0c;建议投资者谨慎决策”的平衡分析。这种回答看似全面&#xff0c;实则毫无用处——它没有帮你做出任何决策&#xff0c;只是把信息重新排…

作者头像 李华