我正在编写一个 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();

sql-server -  Entity Framework  Multi-Tenancy 自定义共享表-LMLPHP

当我尝试在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,首先从数据库中加载已过滤的VendorFavVendors,然后根据传递的对象FavVendors内容添加新的,更新或删除现有的FavVendor记录。

概括地说,它是可行的,但是难以实现和维护(尤其是如果您需要在返回返回其他引用Vendor的其他实体的查询中包括FavVendors和过滤的Vendor时,因为您不能使用典型的Include方法)。您可能会考虑尝试一些像Entity Framework Plus这样的第三方软件包,该软件包具有查询过滤器和包含查询过滤器功能,可以大大简化查询部分。

10-06 03:08