ユーザにロールを設定する
状況
- ユーザにロールを設定する処理を、スマートに書きたい
- ASP.NETメンバーシップのスキーマを利用
- NHibernateを利用
- アプリケーションで使用するロールは決まっている
やりたいこと
↓のようなコードではなく…
User user = _repository.FindByName(userName); Role eatRole = _roleRepository.FindByRoleName("Eat"); Role cookRole = _roleRepository.FindByRoleName("Cook"); user.AddRole(eatRole); user.AddRole(cookRole); Commit();
↓のようなコードを書きたい
User user = _repository.FindByName(userName); user.SetSurviveRole(); Commit();
Public class User { private IList<Role> _roles = new List<Role>(); Public void SetSurviveRole() { _roles.Add(Role.Eat); _roles.Add(Role.Cook); } }
- アプリケーションで利用するロールは既に決まっているので、ロールリポジトリは使用したくない
- これだとテストも簡単
必要なこと
- イミュータブルなRoleクラスを用意
- Roleクラスには、データベースとマッピングするための識別子、Idをもたせる
public class Role { public Role() { } public Role(Guid id) { if (id == null || id == Guid.Empty) throw new ArgumentNullException(); _id = id; } private readonly Guid _id; public readonly static Role Eat = new Role(Guid.Parse("7D115923-3C09-4225-B4F9-2AAD955AD1DC")); public readonly static Role Cook = new Role(Guid.Parse("72AB766F-71D8-4824-8750-9190BEFDCEF1")); /// <summary> /// Id /// </summary> public Guid Id { get { return _id; } } /// <summary> /// 名称 /// </summary> public String Name { get { return _roleNames[_id]; } set { } } .... .... あとEqualsとか、ToStringなどのoverrideメソッド }
- Userクラスの_rolesを、データベースとマッピングさせる
- Userクラスの_rolesの型を、以下の用に変更
private Iesi.Collections.Generic.ISet<Role> _roles = new HashedSet<Role>();
- 結合テーブル(aspnet_UsersInRoles)を使ったmany-to-manyのマッピングを行う
- Userクラスのマッピングファイル User.hbm.xmlの中身
<class name="User" table="aspnet_Users" lazy="false"> <id name="Id" column="UserId"> <generator class="guid"></generator> </id> <set name="Roles" access="field.camelcase-underscore" table="aspnet_UsersInRoles"> <key column="UserId"/> <element column="RoleId" type="{ネームスペース}.RoleType, {RoleTypeがあるアセンブリ名}"> </element> </class>
Userのマッピングについて
Userクラスの IList
aspnet_UsersInRolesテーブルのRoleId(Guid)から、Roleクラスをインスタンス化しなくてはいけないし、
Roleクラスのインスタンスから、RoleId取得しないといけない。
つまり、RoleId は Roleクラス型だよ、みたいなカスタム型を作成しなくてはいけない。
ということで、カスタム型を実装する。
NHibernate 5.2.3. カスタム型
(aspnet_RolesテーブルとRoleクラスのマッピングを定義する方法もあるが、
それだとNHibernateがRoleの状態を管理しようとするらしく、よろしくないらしい。
RoleはEntityではなく、ValueObjectなので、上記の用にする)
カスタム型 RoleType
NHibernate.UserTypes.IUserTypeを実装したクラスを作成する
public class RoleType : IUserType { public object Assemble(object cached, object owner) { return DeepCopy(cached); } public object DeepCopy(object value) { return value; } public object Disassemble(object value) { return DeepCopy(value); } public bool Equals(object x, object y) { return OperatorHelper.Equal(x, y); } public int GetHashCode(object x) { if (x == null) return 0; return x.GetHashCode(); } public bool IsMutable { get { return false; } } public object NullSafeGet(System.Data.IDataReader rs, string[] names, object owner) { Guid roleId = Guid.Empty; Guid.TryParse(Convert.ToString(NHibernateUtil.String.NullSafeGet(rs, names[0])), out roleId); if (roleId == Guid.Empty) return null; return Role.GetBy(roleId); } public void NullSafeSet(System.Data.IDbCommand cmd, object value, int index) { IDataParameter valueParam = (IDataParameter)cmd.Parameters[index]; if (valueParam == null) { valueParam.Value = DBNull.Value; } else { valueParam.Value = ((Role)value).Id; } } public object Replace(object original, object target, object owner) { return original; } public Type ReturnedType { get { return typeof(Role); } } public global::NHibernate.SqlTypes.SqlType[] SqlTypes { get { return new global::NHibernate.SqlTypes.SqlType[] { new global::NHibernate.SqlTypes.SqlType(DbType.Guid) }; } } }
詳細は、理解していないので説明できない…。
重要なのは、NullSafeGetメソッドと、NullSafeSetメソッド。
以上。
おまけ
データベースの文字列の値を、Enumのプロパティにバインドしたい時は、
NHibernate.Type.EnumStringTypeを実装することでできる