13

私は Entity Framework を使用しており、同じ型の別のレコードを参照して親子階層を形成できる BusinessUnits のテーブルを持っています。

また、一連のユーザーとユーザー権限もあり、このテーブルで定義された各ユーザーは、階層内の 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
*/
}
4

6 に答える 6

12

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(またはエンティティ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:エンティティをプリロードする

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

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

        using (BusinessModelContainer bm = new BusinessModelContainer())
        {
            List<BusinessUnit> allBusinessUnits = bm.BusinessUnits.ToList();

            var userWithPermissions = (from u in bm.Users.Include("UserPermissions")
                                       where u.UserID == 1234
                                       select u).Single();

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

            var explicitlyPermittedUnits = from p in userWithPermissions.UserPermissions
                                           select p.BusinessUnit;

            foreach (var bu in explicitlyPermittedUnits)
            {
                unitsForUser.Add(bu);
                unitsForUser.AddRange(GetChildren(bm, bu));
            }

            var distinctUnitsForUser = (from bu in unitsForUser
                                        group bu by bu.BusinessUnitID into q
                                        select q.First()).ToList();
        }

上記の2つの例は改善される可能性がありますが、作業を進めるための例として役立つことに注意してください。

オプション3:共通テーブル式を使用した特注のSQLクエリ

多数のビジネスユニットがある場合は、最も効率的な方法を試してみることをお勧めします。これは、階層共通テーブル式を使用して1回のヒットで情報を取得するカスタムSQLを実行することです。もちろん、これは実装を1つのプロバイダー(おそらくSQL Server)に結び付けます。

SQLは次のようになります。

    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オブジェクトのコレクションを実体化します。

bm.BusinessUnits.SqlQuery(mySqlString, userId);

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

概要

どの方法が最も効果的かを判断するために、現実的なデータセットを使用したテストに勝るものはありません。手作りのSQLコード(オプション3)を使用すると、常に最高のパフォーマンスが得られる可能性がありますが、移植性の低い、より複雑なコードが必要になります(基盤となるdbテクノロジーに関連付けられているため)。

また、使用できるオプションは、使用しているEFの「フレーバー」、およびもちろん、選択したデータベースプラットフォームによって異なることにも注意してください。これを説明するより具体的なガイダンスが必要な場合は、追加情報で質問を更新してください。

  • どのデータベースを使用していますか?
  • プロジェクトではEDMXファイルを使用しますか、それとも最初にコードを使用しますか?
  • EDMXを使用している場合、デフォルトの(EntityObject)コード生成手法を使用しますか、それともT4テンプレートを使用しますか?
于 2012-09-11T19:11:51.623 に答える
2

私の理解が正しければ、必要なのは再帰クエリ (生の T-SQL での再帰共通テーブル式) です。私の知る限り、純粋な LINQ to Entities でこのような再帰クエリを記述する方法はありません。

ただし、階層の最大深度がわかっている場合は、一定回数結合する単一のクエリを作成して、目的の結果を得ることができます。

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 を簡単にクエリできます。

于 2012-09-07T23:36:05.060 に答える
0

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

于 2012-09-11T19:23:57.420 に答える
0

階層的なjsonデータをWebに返す問題を解決する必要があり、共通式テーブル(CET)を使用するというOllyの提案を使用することから始めました。私のコードは

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

しかし、子にアクセスするときに、Web アプリがまだデータベースへの往復を行っていることに気付きました。Entity オブジェクトを Json に渡すだけでも面倒です。多くの場合、不要なフィールドになってしまうからです。

私が思いついた最終的な解決策は、CET を必要とせず、DB への 1 回の旅行のみを行います。私の場合、SurveyId に基づいてすべてのレコードを取得できましたが、使用するキーがない場合でも、CET を使用して階層を取得できます。

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

1) まず、必要なレコードをデータベースからロードします。

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;

結果:

ブラウザ上の階層

于 2014-06-30T16:31:04.617 に答える
0

ソリューションにlinqを使用することに縛られていない場合は、SQLでCTEをそのまま使用する方がはるかに簡単で高速です。

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]]));
于 2012-09-11T19:39:05.790 に答える
0

SQL の再帰的 CTE は、基本ルールを使用した単なる手法です。これらの基本ルールを使用して、LINQ で同じクエリを作成できます。

従うべき簡単な手順は次のとおりです

1) UserPermissions テーブルからパーミッションのリストを取得する 2) Foreach パーミッション、ツリーを再帰的にパーミッションのサブセットを見つける

これらのクエリを最適化/適応させる方法はたくさんありますが、コアは次のとおりです。

//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 ユーザー 1 を照会した場合 -> (B,C) のアクセス許可 (図を参照)

サンプル階層

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] });
        }
    }
于 2012-09-11T21:13:51.500 に答える