Entity Framework Трассировка и возврат дочерних записей в собственной справочной таблице

Я использую Entity Framework и имею таблицу BusinessUnits, которая может ссылаться на другую запись того же типа, чтобы сформировать иерархию дочернего родителя.

У меня также есть набор пользователей и пользовательские разрешения, в которых каждый пользователь, определенный в этой таблице, должен иметь доступ к BusinessUnit и всем подразделениям бизнес-подразделения в иерархии. Пользователи не должны иметь доступ к BusinessUnit над ссылкой (если существует).

Как я могу начать формирование запросов LINQ для обработки этого дерева привязки с привязкой и вернуть все бизнес-единицы (с дочерними блоками), для которых этот пользователь имеет доступ? Возможно ли это сделать в одном запросе или мне нужно вручную создать дерево с помощью цикла for?

Я видел ссылку схемы таким образом от node до родителя, означает ли это, что мне нужно начинать с самого дальнего дочернего node, чтобы построить дерево одним родителем за раз?

Спасибо заранее,

Крис

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
*/
}

Ответ 1

Хорошо, здесь есть несколько вещей. Мы можем сделать это немного легче, добавив еще несколько свойств в вашу модель. Это вариант? Если это так, добавьте свойства коллекции к объектам. Теперь я не знаю, какой EF API вы используете: DbContext (код first или edmx) или ObjectContext. В моем примере я использовал DbContext API с моделью edmx для создания этих классов.

Если вы предпочитаете, с помощью нескольких аннотаций вы можете обойтись без файла 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. Предварительные загрузки

Однако есть некоторые способы, которыми вы могли бы оптимизировать это, чтобы избежать повторных поездок на сервер. Если в базе данных имеется только небольшое количество бизнес-единиц, вы можете загрузить весь список. Затем, из-за способности 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();
        }

Обратите внимание, что приведенные выше два примера могут быть улучшены, но служат примером для вас.

Вариант 3: запрос SQL запросов с использованием выражения Common Table

Если у вас есть большое количество бизнес-единиц, вы можете попробовать наиболее эффективный метод. Это будет выполнение пользовательского SQL, который использует иерархическое выражение Common Table, чтобы получить информацию за один раз. Это, конечно, связывает реализацию с одним провайдером, возможно, с 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?

Ответ 2

Если я правильно понимаю, то вам нужен рекурсивный запрос (рекурсивное общее табличное выражение в необработанном T-SQL). Насколько я знаю, нет никакого способа написать такой рекурсивный запрос в чистом LINQ для 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 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. Однако, вероятно, будет лучше выполнить 1 запрос на уровень иерархии в цикле for.

Если вы не знаете MAX_DEPTH, вы можете подумать о добавлении столбца глубины в таблицу бизнес-единиц (легко установить при вставке, поскольку она всегда parent.depth + 1). Затем вы можете легко запросить MAX_DEPTH перед запуском разрешающего запроса.

Ответ 3

Чтобы получить иерархию в одном запросе, вам нужно использовать специальную структуру таблицы. Одним из возможных решений является наличие специального ключа, который содержит всех родителей этой записи. В этом случае у вас есть простой и очень быстрый (он будет быстрее, чем рекурсия cte), чтобы получить все дочерние элементы.
Но если вы хотите переместить запись в другую ветвь иерархии, это будет очень экспансивная операция.

Ответ 4

Если вы не привязаны к использованию linq для решения, его гораздо проще и быстрее использовать CTE в sql как таковой:

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

Ответ 5

Рекурсивный CTE в SQL - это просто метод с использованием основных правил. Вы можете создать тот же запрос в LINQ, используя эти основные правила.

Ниже приведены простые шаги, которые следует выполнить

1) Получить список разрешений из таблицы UserPermissions 2) Предварительное разрешение, рекурсия дерева, чтобы найти подмножество разрешений

Существует много способов optmize\адаптировать эти запросы, но вот ядро:

//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) (см. диаграмму)

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

Ответ 6

Мне пришлось решить проблему возврата иерархических json-данных в сеть, и я начал с использования предложения Olly об использовании таблицы Common Expression (CET), а мой код был

    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 и только делает одно путешествие к БД. В моем случае я мог бы вытащить все записи на основе 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) Создайте для него словарь ViewModels.

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;

Результат:

Hierarchy on the browser