13

我正在使用实体框架并有一个 BusinessUnits 表,它可以引用同一类型的另一条记录以形成子父层次结构。

我还有一组用户和用户权限,在此表中定义的每个用户都应该有权访问业务单元和层次结构中的所有子业务单元。用户不应访问所引用的业务单元(如果存在)之上的业务单元。

如何形成 LINQ 查询来处理这个自引用关系树并返回该用户有权访问的所有业务单位(带有子单位)?是否可以在一个查询中完成,或者我是否需要自己使用 for 循环手动构建树?

我已经从节点到父节点以这种方式看到模式的引用,这是否意味着我必须从最远的子节点开始一次由一个父节点构建树?

提前致谢,

克里斯

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

好的,这里有几件事。我们可以通过向您的模型添加更多属性来简化此操作。这是一个选择吗?如果是这样,请将集合属性添加到实体。现在,我不知道您使用的是哪个 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:预加载实体

但是,有一些方法可以优化它以避免重复访问服务器。如果您在数据库中只有少量的业务单位,您可以加载整个列表。然后,由于 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 查询

如果您有大量业务单位,您可能想尝试最有效的方法。那将是执行自定义 SQL,该 SQL 使用分层公用表表达式一键取回信息。这当然会将实现绑定到一个提供程序,可能是 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)总是可能表现最好,但代价是拥有更复杂的代码,移植性较差(因为它与底层数据库技术相关联)。

另请注意,您可用的选项取决于您正在使用的 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

要在单个请求中获取层次结构,您需要使用特殊的表结构。一种可能的解决方案是拥有一个包含此记录的所有父项的特殊密钥。在这种情况下,您可以使用简单且非常快速(它比 cte 递归更快)的查询来获取所有子项。
但是,如果您想将记录移动到层次结构的另一个分支,这将是非常广泛的操作。

于 2012-09-11T19:23:57.420 回答
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;
        }

查询用户 1 时的 O\p -> (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 回答
0

我必须解决将分层 json 数据返回到网络的问题,我首先使用 Olly 建议使用通用表达式表 (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();
    }

但是我注意到在访问孩子时,Web 应用程序仍在对数据库进行往返!将 Entity 对象传递给 Json 也很痛苦,因为很多人最终会得到不需要的字段。

我想出的最终解决方案不需要 CET,只需要一次访问数据库。就我而言,我可以根据 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) 为其创建一个 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;

结果:

浏览器上的层次结构

于 2014-06-30T16:31:04.617 回答