2012-05-24 10 views
11

私はEntity Frameworkを使用しており、同じタイプの別のレコードを参照できるBusinessUnitsのテーブルを持ち、子 - 親の階層を形成しています。Entity Framework自己参照テーブル内の子レコードをトラバースして返す

また、この表に定義されている各ユーザーは、階層内のBusinessUnitおよびすべてのサブビジネスユニットにアクセスできるユーザーとユーザー権限を持っています。ユーザーは、参照されているBusinessUnit(存在する場合)の上にあるBusinessUnitにアクセスすべきではありません。

この自己参照関係ツリーを処理し、このユーザーがアクセスできるすべてのビジネス単位(子単位)を返すようLINQクエリを作成するにはどうすればよいですか? 1つのクエリでそれを行うことは可能ですか、forループを使用して手動でツリーを構築する必要がありますか?

私はこのようにノードから親にスキーマの参照を見てきました。これは一度に1つの親によってツリーを構築するために最も遠い子ノードから始める必要がありますか?あなたが欲しいものを事前に

おかげで、

クリス

class BusinessUnit 
{ 
    int BusinessUnitID {get;set;} 
    public string BusinessName {get;set;} 
    BusinessUnit ParentBusinessUnit {get;set;} 
} 

class User 
{ 
    int UserID {get;set;} 
    string Firstname {get;set;} 
} 

class UserPermissions 
{ 
    [Key, ForeignKey("BusinessUnit"), Column(Order = 0)] 
    BusinessUnit BusinessUnit {get;set;} 
    [Key, ForeignKey("User"), Column(Order = 1)] 
    User User {get;set;} 
} 

IEnumerable<BusinessUnit> GetUnitsForWhichUserHasAccess(User user) 
{ 
/* Example 1 
given: BusinessUnitA (ID 1) -> BusinessUnitB (ID 2) -> BusinessUnitC (ID 3) 
with user with ID 1: 
and UserPermissions with an entry: BusinessUnit(2), User(1) 
the list { BusinessUnitB, BusinessUnitC } should be returned 
*/ 

/* Example 2 
given: BusinessUnitA (ID 1) -> BusinessUnitB (ID 2) -> BusinessUnitC (ID 3) 
with user with ID 1: 
and UserPermissions with an entry: BusinessUnit(1), User(1) 
the list { BusinessUnitA, BusinessUnitB, BusinessUnitC } should be returned 
*/ 
} 

答えて

10

OK、ここにはいくつかのものがあります。モデルにいくつかのプロパティを追加することで、これを少し簡単にすることができます。それはオプションですか?その場合は、コレクションプロパティをエンティティに追加します。今、どのEF APIを使用しているのかわかりません:DbContext(コードファーストまたはedmx)またはObjectContext。私のサンプルでは、​​これらのクラスを生成するためにedmxモデルでDbContext APIを使用しました。

いくつかの注釈があれば、edmxファイルを省略することもできます。

public partial class BusinessUnit 
{ 
    public BusinessUnit() 
    { 
     this.ChlidBusinessUnits = new HashSet<BusinessUnit>(); 
     this.UserPermissions = new HashSet<UserPermissions>(); 
    } 

    public int BusinessUnitID { get; set; } 
    public string BusinessName { get; set; } 
    public int ParentBusinessUnitID { get; set; } 

    public virtual ICollection<BusinessUnit> ChlidBusinessUnits { get; set; } 
    public virtual BusinessUnit ParentBusinessUnit { get; set; } 
    public virtual ICollection<UserPermissions> UserPermissions { get; set; } 
} 

public partial class User 
{ 
    public User() 
    { 
     this.UserPermissions = new HashSet<UserPermissions>(); 
    } 

    public int UserID { get; set; } 
    public string FirstName { get; set; } 

    public virtual ICollection<UserPermissions> UserPermissions { get; set; } 
} 

public partial class UserPermissions 
{ 
    public int UserPermissionsID { get; set; } 
    public int BusinessUnitID { get; set; } 
    public int UserID { get; set; } 

    public virtual BusinessUnit BusinessUnit { get; set; } 
    public virtual User User { get; set; } 
} 

public partial class BusinessModelContainer : DbContext 
{ 
    public BusinessModelContainer() 
     : base("name=BusinessModelContainer") 
    { 
    } 

    protected override void OnModelCreating(DbModelBuilder modelBuilder) 
    { 
     throw new UnintentionalCodeFirstException(); 
    } 

    public DbSet<BusinessUnit> BusinessUnits { get; set; } 
    public DbSet<User> Users { get; set; } 
    public DbSet<UserPermissions> UserPermissions { get; set; } 
} 

@Chaseメダルは、再帰LINQ(またはEntity SQL)クエリを記述することはできません。

オプション1:遅延ロード

遅延ロードでは、あなたがこのような何かを行うことができ、有効になって...

private static IEnumerable<BusinessUnit> UnitsForUser(BusinessModelContainer container, User user) 
    { 
     var distinctTopLevelBusinessUnits = (from u in container.BusinessUnits 
              where u.UserPermissions.Any(p => p.UserID == user.UserID) 
              select u).Distinct().ToList(); 

     List<BusinessUnit> allBusinessUnits = new List<BusinessUnit>(); 

     foreach (BusinessUnit bu in distinctTopLevelBusinessUnits) 
     { 
      allBusinessUnits.Add(bu); 
      allBusinessUnits.AddRange(GetChildren(container, bu)); 
     } 

     return (from bu in allBusinessUnits 
       group bu by bu.BusinessUnitID into d 
       select d.First()).ToList(); 
    } 

    private static IEnumerable<BusinessUnit> GetChildren(BusinessModelContainer container, BusinessUnit unit) 
    { 
     var eligibleChildren = (from u in unit.ChlidBusinessUnits 
           select u).Distinct().ToList(); 

     foreach (BusinessUnit child in eligibleChildren) 
     { 
      yield return child; 

      foreach (BusinessUnit grandchild in child.ChlidBusinessUnits) 
      { 
       yield return grandchild; 
      } 
     } 
    } 

オプション2:プリロードエンティティ

はしかし、あなたがサーバーに繰り返しトリップを回避するために、これを最適化することができ、いくつかの方法があります。あなたがデータベースにreasaonably少数のビジネスユニットしか持っていない場合は、リスト全体をロードすることができます。そして、関係を自動的に修正するEFの能力のために、データベースからユーザーとその権限を読み込むだけで、私たちに必要なものすべてが与えられます。

明確にする:このメソッドは、すべてBusinessUnitエンティティをロードすることを意味します。ユーザーに許可されていないものであっても。ただし、SQL Serverでは「チャタリング」が大幅に減少するため、上記のオプション1よりも優れたパフォーマンスを発揮する可能性があります。以下のオプション3とは異なり、これは特定のプロバイダに依存しない「純粋な」EFです。

上記の2つの例は改善される可能性があることにご注意ください。

オプション3:あなたはビジネスユニットの数が多い場合は別注SQLクエリは、共通テーブル式

を使用して、あなたが最も効率的な方法をしようとする場合があります。これは、階層的な共通表式を使用して1回のヒットで情報を戻すカスタムSQLを実行することです。これはもちろん、実装を1つのプロバイダ、おそらくSQL Serverに結びつけます。

WITH UserBusinessUnits 
      (BusinessUnitID, 
      BusinessName, 
      ParentBusinessUnitID) 
      AS 
      (SELECT Bu.BusinessUnitId, 
        Bu.BusinessName, 
        CAST(NULL AS integer) 
        FROM Users U 
        INNER JOIN UserPermissions P ON P.UserID = U.UserID 
        INNER JOIN BusinessUnits Bu ON Bu.BusinessUnitId = P.BusinessUnitId 
        WHERE U.UserId = ? 
      UNION ALL 
      SELECT Bu.BusinessUnitId, 
        Bu.BusinessName, 
        Bu.ParentBusinessUnitId 
        FROM UserBusinessUnits Uu 
        INNER JOIN BusinessUnits Bu ON Bu.ParentBusinessUnitID = Uu.BusinessUnitId) 
    SELECT DISTINCT 
      BusinessUnitID, 
      BusinessName, 
      ParentBusinessUnitID 
      FROM UserBusinessUnits 

あなたは、ユーザーが権限を持っているBusinessUnitオブジェクトのコレクションをマテリアライズするには、以下のようなコードを使用します。

あなたのSQLはこのようなものになるだろう。

bm.BusinessUnits.SqlQuery(mySqlString, userId); 

上記の行と@Jeffreyが提案した非常に類似したコードとの間には微妙な違いがあります。上記のものはDbSet.SqlQuery()を使用し、彼の使用方法はDatabase.SqlQueryです。後者は、コンテキストによって追跡されないエンティティを生成しますが、前者は(デフォルトでは)エンティティを追跡します。トラッキングされたエンティティは、変更を作成して保存する機能、およびナビゲーションプロパティの自動修正機能を提供します。これらの機能が必要ない場合は、変更トラッキングを無効にしてください(.AsNoTracking()またはDatabase.SqlQueryを使用)。

概要

何が最も効果的である方法を決定するために設定され、現実的なデータでテストに勝るものはありません。手作業で作成したSQLコード(オプション3)を使用すると、パフォーマンスは最高になりますが、移植性の低い複雑なコードを犠牲にしています(基本的なdbテクノロジに結びついているため)。

利用可能なオプションは、使用しているEFの "味"と、もちろん選択したデータベースプラットフォームによって異なります。これを説明する具体的なガイダンスをご希望の場合は、追加情報で質問を更新してください。

  • どのデータベースをお使いですか?
  • EDMXファイルを使用してプロジェクトを作成するか、最初にコードを作成しますか?
  • EDMXを使用している場合は、デフォルト(EntityObject)のコード生成手法、つまりT4テンプレートを使用しますか?
+0

参照.Single ();))、この仮定はOPには述べられていない。 –

+0

@Andy: 'Single()'の呼び出しは、有効なプライマリキーを使用して単一のユーザを選択することです。パーミッションへの参照は '.Include()'メソッドにあります。その結果、ユーザのすべてのパーミッションがクエリに含まれます。 – Olly

+0

あなたは..私の悪い! –

1

私が正しく理解していた場合には、再帰クエリ(生T-SQLでの再帰共通テーブル式)です。私が知る限り、このような再帰クエリを純粋なLINQにエンティティに書き込む方法はありません。

ただし、階層の最大深さを知っている場合は、一定の回数だけ結合して必要な結果を得ることができます。

int userIdOfInterest = ... 
IQueryable<BusinessUnit> units = ... 

// start with a query of all units the user has direct permission to 
var initialPermissionedUnits = units.Where(bu => bu.UserPermissions.Any(up => up.User.Id == userIdOfInterest)); 

var allHierarchyLevels = new Stack<IQueryable<BusinessUnit>(); 
allHierarchyLevels.Push(initialPermissionedUnits); 
for (var i = 0; i < MAX_DEPTH; ++i) { 
    // get the next level of permissioned units by joining the last level with 
    // it's children 
    var nextHierarchyLevel = allHierarchyLevels.Peek() 
      // if you set up a Children association on BusinessUnit, you could replace 
      // this join with SelectMany(parent => parent.Children) 
      .Join(units, parent => parent.BusinessUnitId, child => child.ParentBusinessUnit.BusinessUnitId, (parent, child) => child)); 
    allHierarchyLevels.Push(nextHierarchyLevel); 
} 

// build an IQueryable<> which represents ALL units the query is permissioned too 
// by UNIONING together all levels of the hierarchy (the UNION will eliminate duplicates as well) 
var allPermissionedUnits = allHierarchyLevels.Aggregate((q1, q2) => q1.Union(q2)); 

// finally, execute the big query we've built up 
return allPermissionedUnits.ToList(); 

もちろん、生成されたクエリのパフォーマンスは、MAX_DEPTHが増加するにつれて悪化する可能性があります。しかし、おそらくforループの階層のレベルごとに1つのクエリを実行する方が良いでしょう。

MAX_DEPTHがわからない場合は、ビジネスユニットテーブルに深さの列を追加することを検討してください(挿入時にはparent.depth + 1なので簡単に設定できます)。次に、許可クエリを実行する前に簡単にMAX_DEPTHを問い合わせることができます。

0

単一の要求で階層を取得するには、特別なテーブル構造を使用する必要があります。可能な解決策の1つは、このレコードのすべての親を含む特別なキーを持つことです。この場合、すべての子を取得するために、単純で非常に高速です(cte再帰より高速です)。
しかし、階層の別のブランチにレコードを移動したい場合は、非常に広範な操作になります。

0

あなたのようなSQLでCTEを使用するために、その非常に簡単かつ迅速に、解決のためのLINQを使用することに縛られていない場合:

var sql = @" 
WITH BusinessUnitHierarchy (BusinessUnitID, BusinessName, ParentBusinessUnitID) 
AS(
    Select bu.BusinessUnitID, bu.BusinessName, bu.ParentBusinessUnitID 
    from BusinessUnit bu 
    inner join [UserPermissions] up on bu.BusinessUnitID = up.BusinessUnitID 
    where up.UserID = @userID 
    UNION ALL 

    Select 
    bu.BusinessUnitID, bu.BusinessName, bu.ParentBusinessUnitID 
    from BusinessUnit bu 
    inner join BusinessUnitHierarchy buh on bu.ParentBusinessUnitID = buh.BusinessUnitID 
) 
SELECT * FROM BusinessUnitHierarchy buh 
"; 
context.Database.SqlQuery<BusinessUnit>(sql, new SqlParameter("userID", [[your user ID here]])); 
0

SQLでの再帰CTEは、グランドルールを使用して、単に技術です。これらの基本ルールを使用して、LINQで同じクエリを構築できます。ここで

方法の多くは、にあります許可のサブセットを見つけるために)

1に従うUserPermissionsテーブルから 2)foreachの許可を権限の一覧を取得し、ツリーを再帰的にするための簡単な手順です

(図を参照)(B、C)に>パーミッション - ユーザ1のために照会する

//Gets the list of permissions for this user 
     static IEnumerable<BusinessUnit> GetPermissions(int userID) 
     { 
      //create a permission tree result set object 
      List<BusinessUnit> permissionTree = new List<BusinessUnit>(); 

      //Get the list of records for this user from UserPermissions table 
      IEnumerable<UserPermissions> userPermissions = from UP in UPs 
             where UP.User.UserID == userID 
             select UP; 

      //for each entry in UserPermissions, build the permission tree 
      foreach (UserPermissions UP in userPermissions) 
      { 
       BuildPermissionTree(UP.BusinessUnit, permissionTree); 
      } 

      return permissionTree; 
     } 

//recursive query that drills the tree. 
     static IEnumerable<BusinessUnit> BuildPermissionTree(BusinessUnit pBU,List<BusinessUnit> permissionTree) 
     { 
      permissionTree.Add(pBU); 

      var query = from BU in BUs 
         where BU.ParentBusinessUnit == pBU 
         select BU; 

      foreach (var BU in query) 
      { 
       BuildPermissionTree(BU,permissionTree); 
      } 
      return permissionTree; 
     } 

Oの\のP:optmize \これらのクエリを適合させるが、ここではコアでありますここ

Sample Heirarchy

BusinessUnitB 
BusinessUnitG 
BusinessUnitC 
BusinessUnitD 
BusinessUnitF 
BusinessUnitE 

完全なコードです:

class BusinessUnit 
    { 
     public int BusinessUnitID { get; set; } 
     public string BusinessName { get; set; } 
     public BusinessUnit ParentBusinessUnit { get; set; } 

     public override string ToString() 
     { 
      return BusinessUnitID + " " + BusinessName + " " + ParentBusinessUnit; 
     } 
    } 

    class User 
    { 
     public int UserID { get; set; } 
     public string Firstname { get; set; } 

     public override string ToString() 
     { 
      return UserID + " " + Firstname; 
     } 
    } 

    class UserPermissions 
    { 
     public BusinessUnit BusinessUnit { get; set; } 
     public User User { get; set; } 

     public override string ToString() 
     { 
      return BusinessUnit + " " + User; 
     } 
    } 

    class SOBUProblem 
    { 
     static List<BusinessUnit> BUs = new List<BusinessUnit>(); 
     static List<User> Users = new List<User>(); 
     static List<UserPermissions> UPs = new List<UserPermissions>(); 

     static void Main() 
     { 
      //AutoInitBU(); 
      InitBU(); 
      InitUsers(); 
      InitUPs(); 
      //Dump(BUs); 
      //Dump(Users); 
      //Dump(UPs); 
      //SpitTree(BUs[2]); 
      int userID = 1; 
      foreach (var BU in GetPermissions(userID)) 
       Console.WriteLine(BU.BusinessName); 

     } 
     //Gets the lsit of permissions for this user 
     static IEnumerable<BusinessUnit> GetPermissions(int userID) 
     { 
      //create a permission tree result set object 
      List<BusinessUnit> permissionTree = new List<BusinessUnit>(); 

      //Get the list of records for this user from UserPermissions table 
      IEnumerable<UserPermissions> userPermissions = from UP in UPs 
             where UP.User.UserID == userID 
             select UP; 

      //for each entry in UserPermissions, build the permission tree 
      foreach (UserPermissions UP in userPermissions) 
      { 
       BuildPermissionTree(UP.BusinessUnit, permissionTree); 
      } 

      return permissionTree; 
     } 

     //recursive query that drills the tree. 
     static IEnumerable<BusinessUnit> BuildPermissionTree(BusinessUnit pBU,List<BusinessUnit> permissionTree) 
     { 
      permissionTree.Add(pBU); 

      var query = from BU in BUs 
         where BU.ParentBusinessUnit == pBU 
         select BU; 

      foreach (var BU in query) 
      { 
       BuildPermissionTree(BU,permissionTree); 
      } 
      return permissionTree; 
     } 

     static void Dump<T>(IEnumerable<T> items) 
     { 
      foreach (T item in items) 
      { 
       Console.WriteLine(item.ToString()); 
      } 
     } 

     static void InitBU() 
     { 
      BusinessUnit BURoot = new BusinessUnit() { BusinessUnitID = 1, BusinessName = "BusinessUnitA" }; 
      BUs.Add(BURoot); 
      BusinessUnit BUlevel11 = new BusinessUnit() { BusinessUnitID = 2, BusinessName = "BusinessUnitB", ParentBusinessUnit = BURoot }; 
      BusinessUnit BUlevel12 = new BusinessUnit() { BusinessUnitID = 3, BusinessName = "BusinessUnitC", ParentBusinessUnit = BURoot }; 
      BUs.Add(BUlevel11); 
      BUs.Add(BUlevel12); 
      BusinessUnit BUlevel121 = new BusinessUnit() { BusinessUnitID = 4, BusinessName = "BusinessUnitD", ParentBusinessUnit = BUlevel12 }; 
      BusinessUnit BUlevel122 = new BusinessUnit() { BusinessUnitID = 5, BusinessName = "BusinessUnitE", ParentBusinessUnit = BUlevel12 }; 
      BUs.Add(BUlevel121); 
      BUs.Add(BUlevel122); 
      BusinessUnit BUlevel1211 = new BusinessUnit() { BusinessUnitID = 6, BusinessName = "BusinessUnitF", ParentBusinessUnit = BUlevel121 }; 
      BUs.Add(BUlevel1211); 
      BusinessUnit BUlevel111 = new BusinessUnit() { BusinessUnitID = 7, BusinessName = "BusinessUnitG", ParentBusinessUnit = BUlevel11 }; 
      BUs.Add(BUlevel111); 
     } 

     static void AutoInitBU() 
     { 
      BusinessUnit BURoot = new BusinessUnit() { BusinessUnitID = 1, BusinessName = "BusinessUnitA" }; 
      BUs.Add(BURoot); 
      Dictionary<int, string> transTable = new Dictionary<int, string>() {{2,"B"},{3,"C"} }; 
      //Create Child nodes 
      for (int i = 0; i < 2; i++) 
      { 
       BUs.Add(new BusinessUnit() { BusinessUnitID = i + 2, BusinessName = "BusinessUnit" + transTable[i+2],ParentBusinessUnit = BUs[i]}); 
      } 
     } 

     static void InitUsers() 
     { 
      Users.Add(new User() {UserID = 1,Firstname="User1" }); 
     } 

     static void InitUPs() 
     { 
      UPs.Add(new UserPermissions() { BusinessUnit = BUs[1], User = Users[0] }); 
      UPs.Add(new UserPermissions() { BusinessUnit = BUs[2], User = Users[0] }); 
     } 
    } 
+0

原則は問題ありませんが、Linq to Entities (またはほとんどのデータベースLinqプロバイダ)、データベースへの複数回のトリップが必要になります。これは受け入れられるかもしれません。 EFと一緒に再帰的なCTE(単一のデータベースクエリ)を使用する代わりに私の答えを見てください。 – Olly

+0

ありがとうございます。私は私の記事の最適化についても述べており、私は私のものが最適化されていないと考えています。しかし、私はちょっとあなたの声明に興味があります**サーバーへの1回の旅行**。 '(BusinessModelContainer bm = new BusinessModelContainer()) 'を使用して開始するクエリがオプトマイズされたクエリであると仮定すると、いくつかの列挙があり、すべての列挙が基になるLINQプロバイダへのクエリ実行であることがわかります。 IMO、 'BusinessUnits.ToList(); u).Single();(GetChildren(bm、bu));を選択します。 q.First()を選択してください。ToList(); 'はすべてサーバーへのトリップです。 –

+0

私の答えの最後にあるコードを見てください。私は3つのソリューションを提供しました。最初の2つはサーバーへの複数回のトリップが必要です。最後のものはそれを避けるためにCTEを使用します。 – Olly

0

私はウェブに階層JSONデータを返すの問題を解決しなければならなかったと私は一般的な発現テーブルを使用してのオリーの提案を使用して始まりました(CET)と私は子供たちにアクセスする際、ウェブアプリはまだDATへのラウンドトリップを作っていた気づい

static public IEnumerable<TagMaster> GetHierarchy(IEnumerable<int> surveyId, Entities dbContext) 
    { 
     var sql = String.Format(@" 
WITH SurveyTags ([TagID], [TagTitle], [SurveyID], [ParentTagID]) AS (
    SELECT [TagID], [TagTitle], [SurveyID], [ParentTagID] 
    FROM [dbo].[TagMaster] 
    WHERE [SurveyID] in ({0}) and ParentTagID is null 
    UNION ALL 
    SELECT 
     TagMaster.[TagID], TagMaster.[TagTitle], TagMaster.[SurveyID], TagMaster.[ParentTagID] 
     FROM [dbo].[TagMaster] 
     INNER JOIN SurveyTags ON TagMaster.ParentTagID = SurveyTags.TagID 
) 
SELECT [TagID], [TagTitle], [SurveyID], [ParentTagID] 
FROM SurveyTags", String.Join(",", surveyId)); 
     return dbContext.TagMasters.SqlQuery(sql).Where(r => r.ParentTagID == null).ToList(); 
    } 

だったが、私のコード根絶! EntityオブジェクトをJsonに渡すだけでは苦労します。なぜなら、たくさんのフィールドを必要としないからです。

私が思いついた最終的な解決策はCETを必要とせず、DBへの1回のトリップに過ぎません。私の場合は、SurveyIdに基づいてすべてのレコードを取り上げることができますが、使用するキーがない場合でも、CETを使用して階層を取得できます。

これは、フラットレコードをツリーに変換して、必要なフィールドだけを取った方法です。

1)まず、必要なレコードをdbからロードします。

var tags = db.TagMasters.Where(r => surveyIds.Contains(r.SurveyID)).Select(r => new { id = r.TagID, name = r.TagTitle, parentId = r.ParentTagID }).ToList(); 

2)ViewModelの辞書を作成します。

var tagDictionary = tags.Select(r => new TagHierarchyViewModel { Id = r.id, Name = r.name }).ToDictionary(r => r.Id); 

3)次に階層に変換します。

foreach (var tag in tags) { 
    if (tag.parentId.HasValue) { 
        tagDictionary[tag.parentId.Value].Tags.Add(tagDictionary[tag.id]); 
    } 
    } 

4)すべての子ノードを削除します。

var tagHierarchy = from td in tagDictionary 
    join t in tags on td.Key equals t.id 
    where t.parentId == null 
    select td.Value; 

結果:、私は( `Uを..select)あなたは` UserPermissions`テーブル内のユーザーのための1つのマッピングがあるだろうと想定していることを

一目で

Hierarchy on the browser