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