我正在使用Entity Framework并拥有一个BusinessUnits表,它可以引用相同类型的另一条记录来形成子父级层次结构。
我还有一组用户和用户权限,其中此表中定义的每个用户都应该可以访问BusinessUnit和层次结构中的所有子业务单位。用户不应该访问引用的BusinessUnit(如果存在)。
如何构建LINQ查询来处理此自引用关系树并返回此用户可以访问的所有业务单位(包含子单元)?是否可以在一个查询中执行,或者我是否需要使用for循环手动构建树?
我已经看到了schema从节点到父节点的引用,这是否意味着我必须从最远的子节点开始一次由一个父节点构建树?
提前致谢,
克里斯
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
*/
}
答案 0 :(得分:10)
如果您愿意,可以使用一些注释来免除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 medallion是正确的,因为我们无法编写递归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();
}
请注意,以上两个例子可以改进,但作为一个例子让你去。
选项3:使用公用表表达式定制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的“风味”,当然还取决于您选择的数据库平台。如果您想要一些更具体的指导说明,请使用额外信息更新您的问题。
EntityObject
)代码生成技术还是T4模板?答案 1 :(得分: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。
答案 2 :(得分:0)
要在单个请求中获取层次结构,您需要使用特殊的表结构。一种可能的解决方案是使用包含此记录的所有父项的特殊键。在这种情况下,您可以获得所有子项的简单且非常快(它将比cte递归更快)查询。
但是如果你想将记录转移到另一个层次结构中,那将是非常广泛的操作。
答案 3 :(得分: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]]));
答案 4 :(得分: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 - &gt; (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] });
}
}
答案 5 :(得分:0)
我必须解决将hierarchal json数据返回到网络的问题,我开始使用Olly建议使用Common Expression table(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)为它创建一个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;
结果: