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テンプレートを使用しますか?