实体框架多租户自定义共享表

时间:2017-03-08 13:36:35

标签: sql-server entity-framework

我正在撰写多租户申请表。几乎所有表都有“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”来实现

现在尝试在实体框架中进行此设置...

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

enter image description here

当我尝试在FavVendor.Vendor上设置外键时也收到以下错误

  

FavVendor_Vendor_Source ::多重性在关系'FavVendor_Vendor'中的角色'FavVendor_Vendor_Source'中无效。由于Dependent Role属性不是关键属性,因此Dependent Role的多重性的上限必须为'*'。

3 个答案:

答案 0 :(得分:4)

EF自然不支持棘手的问题。 DTO和投影为您提供所需控制的情况之一。仍然存在纯EF解决方案,但必须非常仔细地编程。我将尽力涵盖尽可能多的方面。

让我们从无法开始。

  

这应该可以通过建立“一对一”关系并使外键为“供应商ID”和“帐户ID”来实现

这是不可能的。 物理(商店)关系是one-to-manyVendor(一)到FavVendor(很多)),尽管逻辑关系对于特定的AccountIdone-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; }
}
  

到目前为止,这是我的解决方案,但我必须获取所有喜爱的供应商,因为我无法在EF中编写子查询,然后当我更新记录时,删除正在发生。

上述两个问题都可以通过特殊方式编写代码来解决。

首先,由于nether lazy 渴望加载支持过滤,唯一剩下的选项是explicit loading(在显式加载时应用过滤器中描述相关实体文档部分)或投影并依赖于上下文导航属性修正(实际上显式加载是基于)。为了避免副作用,必须关闭所涉及实体的延迟加载(我已经通过从导航属性中删除virtual关键字来实现这一点)并且数据检索应始终通过新的短期DbContext实例,以消除由相同的导航属性修复功能导致的相关数据的无意加载,我们依赖该功能对FavVendors进行过滤。

话虽如此,这里有一些操作:

检索具有特定AccountId的已过滤FavVendors的供应商:

按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,然后根据传递的对象{{1内容可以添加新内容,更新或删除现有的FavVendors记录。

总结一下,它是可行的,但很难实现和维护(特别是如果您需要在返回引用FavVendor的其他实体的查询中包含Vendor和筛选FavVendors,因为您不能使用典型的Vendor方法。您可以考虑尝试一些第三方软件包,例如Entity Framework Plus ,其查询过滤器包含查询过滤器功能可以显着简化查询部分。

答案 1 :(得分:0)

你的焦点不正确

而不是

 Vendor     TenantVendor     One to many
 Vendor     FavVendor        One to many
 Account    FavVendor        One to many

我认为应该是

 Vendor        TenantVendor     OK
 TenantVendor  FavVendor        One to many
你评论中的

  

获取登录帐户的FavVendor并继续我的快乐方式。

所以每个帐户都有你的私人供应商,因为关系应该在favVendor和TenantVendor之间

您的查询可能有点像

// query
// how do I only get single vendor for tenant?
var vendor = DbContext.TenantVendor
                    .Include(x => x.Vendor) 
                    .Where(x => x.VendorId == [your vendor id])
                    .SingleOrDefault();

// now filter tenant's favorite vendor
// problem: if I update this record later, it deletes all records != account.Id
vendor.TenantVendors= DbContext.FavVendor
                    .Where(x => x.TenantVendor.AccountId = [account id])
                    .ToList();

此处示例EntityFramework地图

public class Vendor
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class TenantVendor
{
    public int Id {get; set;
    public int AccountId { get;set;} 
    public int VendorId{ get;set;} 
    public virtual Vendor Vendor {get;set;}
    public string NickName { get; set; }
}

public class FavVendor
{
    public int Id { get;set; }

  public string NickName { get; set; }
    public int TenantVendorId { get; set; }
    public virtual TenantVendor TenantVendor { get; set; }
}

在DbContext中

....

        protected override void OnModelCreating(DbModelBuilder builder)
        {


            builder.Entity<Vendor>()
                .HasKey(t => t.Id)
                .Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);

            builder.Entity<TenantVendor>()
                .HasKey(t => t.Id)
                .Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);

            builder.Entity<TenantVendor>()
                .HasRequired(me => me.Vendor)
                .WithMany()
                .HasForeignKey(me => me.VendorId)
                .WillCascadeOnDelete(false);

            builder.Entity<FavVendor>()
                .HasKey(t => t.Id)
                .Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);

            builder.Entity<FavVendor>()
                .HasRequired(me => me.TenantVendor)
                .WithMany()
                .HasForeignKey(me => me.TenantVendorId)
                .WillCascadeOnDelete(false);

          }

          ..

我将您的复合键更改为身份键我认为更好但是您选择

答案 2 :(得分:0)

首先,很难回答问题的方式。这里有两个问题,一个是关于自定义字段,另一个是关于收藏的供应商。另外,我必须假设AccountId指的是租户的主键;如果是这样,您可以考虑将AccountId重命名为TenantId以保持一致性。

关于第一部分:

  

有些租户希望将自定义字段添加到供应商记录中。

这取决于需要自定义字段的程度。这是否需要在系统的其他区域。如果是这样,那么这就是像MongoDB这样的NoSQL数据库的好处之一。如果自定义字段只在这一个区域,我会添加一个TenantVendorCustomField表:

public class TenantVendorCustomField
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id {get; set;}
    public int AccountId { get;set;} 
    public int VendorId{ get;set;} 
    public string FieldName { get; set; }
    public string Value {get; set; }

    [ForeignKey("AccountId")]
    public virtual  Tenant Tenant { get; set; }

    [ForeignKey("VendorId")]
    public virtual  Vendor Vendor { get; set; }

}

关于最喜欢的供应商的下一部分:

  

但我必须获取所有喜爱的供应商

我真的想在这里了解更多有关业务需求的信息。每个租户都需要有一个最喜欢的供应商吗?租户可以拥有多个最受欢迎的供应商吗?

根据这些答案,收藏可能是TenantVendor的财产:

public class TenantVendor
{
   public int AccountId { get;set;} 
   public int VendorId{ get;set;} 
   public string NickName { get; set; }
   public bool Favorite {get; set;}
}

var dbVendorQuery = dbContext.TenantVendors
     .Include(x => x.Vendor)
     .Where(x => x.TenantVendor.Favorite && x.TenantVendor.AccountId == _account.Id) ;