我正在编写一个 Multi-Tenancy 应用程序。几乎所有表都具有“AccountId”以指定哪个租户拥有该记录。我有一个表,其中包含所有租户均可访问的“供应商”列表,它没有AccountId。
一些租户希望将自定义字段添加到供应商记录上。
如何在Code First Entity Framework中进行设置?到目前为止,这是我的解决方案,但由于无法在EF中编写子查询,因此我必须获取所有喜欢的供应商,然后在更新记录时会发生删除。
public class Vendor
{
public int Id { get;set;}
public string Name { get; set; }
}
public class TenantVendor
{
public int AccountId { get;set;}
public int VendorId{ get;set;}
public string NickName { get; set; }
}
// query
// how do I only get single vendor for tenant?
var vendor = await DbContext.Vendors
.Include(x => x.TenantVendors)
.SingleAsync(x => x.Id == vendorId);
// now filter tenant's favorite vendor
// problem: if I update this record later, it deletes all records != account.Id
vendor.TenantVendors= vendor.FavoriteVendors
.Where(x => x.AccountId == _account.Id)
.ToList();
我知道我需要使用多列外键,但是在设置时遇到了麻烦。
架构应如下所示。
Vendor
Id
FavVendor
VendorId
AccountId
CustomField1
然后,我可以查询供应商,获取FavVendor作为登录帐户,然后继续愉快地进行下去。
我当前的解决方案为我提供了额外的“Vendor_Id”外键,但设置不正确
应该可以通过建立“一对一”关系并将外键设置为“供应商ID”和“帐户ID”来实现
现在尝试在 Entity Framework 中获取此设置...
public class Vendor
{
public int Id { get; set; }
public string Name { get; set; }
public virtual FavVendor FavVendor { get; set; }
}
public class FavVendor
{
public string NickName { get; set; }
[Key, Column(Order = 0)]
public int VendorId { get; set; }
public Vendor Vendor { get; set; }
[Key, Column(Order = 1)]
public int AccountId { get; set; }
public Account Account { get; set; }
}
// query to get data
var dbVendorQuery = dbContext.Vendors
.Include(x => x.FavVendor)
.Where(x => x.FavVendor == null || x.FavVendor.AccountId == _account.Id) ;
// insert record
if (dbVendor.FavVendor == null)
{
dbVendor.FavVendor = new FavVendor()
{
Account = _account,
};
}
dbVendor.FavVendor.NickName = nickName;
dbContext.SaveChanges();
当我尝试在FavVendor.Vendor上设置外键时也收到以下错误
最佳答案
EF自然不支持棘手的问题。 DTO和投影为您提供所需控制的情况之一。仍然存在纯EF解决方案,但是必须非常仔细地进行编程。我将尽力涵盖尽可能多的方面。
让我们从无法完成的事情开始。
这不可能。尽管特定one-to-many
的逻辑关系是Vendor
,但是物理(存储)关系是FavVendor
(AccountId
(一个)到one-to-one
(许多))。但是EF仅支持物理关系,因此根本无法表示逻辑关系,而逻辑关系是动态的。
很快,该关系必须像您的初始设计中那样为one-to-many
。这是最终的模型:
public class Vendor
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<FavVendor> FavVendors { get; set; }
}
public class FavVendor
{
public string NickName { get; set; }
[Key, Column(Order = 0)]
public int VendorId { get; set; }
public Vendor Vendor { get; set; }
[Key, Column(Order = 1)]
public int AccountId { get; set; }
}
可以通过以特殊方式拧紧代码来解决上述两个问题。
首先,由于nether懒惰或急切加载支持过滤,因此唯一剩下的选项是explicit loading(在明确加载相关文档的部分时应用中的描述)或投影,并依赖于上下文导航属性修正(实际上是显式加载)是根据)。为了避免产生副作用,必须为相关实体关闭延迟加载(我已经通过从导航属性中删除
virtual
关键字来做到这一点),并且数据检索也应始终通过新的短暂DbContext
实例来进行,以消除意外加载由相同的导航属性修正功能(我们依靠其对FavVendors
进行过滤)导致的相关数据的数量。话虽如此,以下是一些操作:
使用特定帐户ID的过滤的FavVendor检索供应商:
要通过ID检索单个供应商,请执行以下操作:
public static partial class VendorUtils
{
public static Vendor GetVendor(this DbContext db, int vendorId, int accountId)
{
var vendor = db.Set<Vendor>().Single(x => x.Id == vendorId);
db.Entry(vendor).Collection(e => e.FavVendors).Query()
.Where(e => e.AccountId == accountId)
.Load();
return vendor;
}
public static async Task<Vendor> GetVendorAsync(this DbContext db, int vendorId, int accountId)
{
var vendor = await db.Set<Vendor>().SingleAsync(x => x.Id == vendorId);
await db.Entry(vendor).Collection(e => e.FavVendors).Query()
.Where(e => e.AccountId == accountId)
.LoadAsync();
return vendor;
}
}
或更笼统地说,对于供应商查询(已应用过滤,订购,分页等):
public static partial class VendorUtils
{
public static IEnumerable<Vendor> WithFavVendor(this IQueryable<Vendor> vendorQuery, int accountId)
{
var vendors = vendorQuery.ToList();
vendorQuery.SelectMany(v => v.FavVendors)
.Where(fv => fv.AccountId == accountId)
.Load();
return vendors;
}
public static async Task<IEnumerable<Vendor>> WithFavVendorAsync(this IQueryable<Vendor> vendorQuery, int accountId)
{
var vendors = await vendorQuery.ToListAsync();
await vendorQuery.SelectMany(v => v.FavVendors)
.Where(fv => fv.AccountId == accountId)
.LoadAsync();
return vendors;
}
}
从断开连接的实体更新特定AccountId的供应商和FavVendor:
public static partial class VendorUtils
{
public static void UpdateVendor(this DbContext db, Vendor vendor, int accountId)
{
var dbVendor = db.GetVendor(vendor.Id, accountId);
db.Entry(dbVendor).CurrentValues.SetValues(vendor);
var favVendor = vendor.FavVendors.FirstOrDefault(e => e.AccountId == accountId);
var dbFavVendor = dbVendor.FavVendors.FirstOrDefault(e => e.AccountId == accountId);
if (favVendor != null)
{
if (dbFavVendor != null)
db.Entry(dbFavVendor).CurrentValues.SetValues(favVendor);
else
dbVendor.FavVendors.Add(favVendor);
}
else if (dbFavVendor != null)
dbVendor.FavVendors.Remove(dbFavVendor);
db.SaveChanges();
}
}
(对于异步版本,只需在相应的
await
方法上使用Async
即可)为了防止删除不相关的
FavVendors
,首先从数据库中加载已过滤的Vendor
的FavVendors
,然后根据传递的对象FavVendors
内容添加新的,更新或删除现有的FavVendor
记录。概括地说,它是可行的,但是难以实现和维护(尤其是如果您需要在返回返回其他引用
Vendor
的其他实体的查询中包括FavVendors
和过滤的Vendor
时,因为您不能使用典型的Include
方法)。您可能会考虑尝试一些像Entity Framework Plus这样的第三方软件包,该软件包具有查询过滤器和包含查询过滤器功能,可以大大简化查询部分。