前面介紹了一些ABP VNext架構上的內容,隨着內容的細化,我們會發現ABP VNext框架中的Entity Framework處理表之間的引用關系還是比較麻煩的,一不小心就容易出錯了,本篇隨筆介紹在ABP VNext框架中處理和用戶相關的多對多的關系處理。
我們這里需要在一個基礎模塊中創建一個崗位管理,崗位需要包含一些用戶,和用戶是多對多的關系,因此需要創建一個中間表來放置他們的關系,如下所示的數據庫設計。
這個是典型的多對多關系的處理,我們來看看如何在在ABP VNext框架中處理這個關系。
1、擴展系統用戶信息
為了模塊間不產生依賴,例如用戶表,遷移dbcontext中使用了IdentityUser,而運行的dbcontext使用了appuser進行了對其的映射,https://github.com/abpframework/abp/issues/1998
因此參照實例模塊Bloging(https://github.com/abpframework/abp/tree/dev/modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/Users)中的BlogUser來擴展一下模塊的用戶對象
public class AppUser : AggregateRoot<Guid>, IUser, IUpdateUserData { public virtual Guid? TenantId { get; protected set; } public virtual string UserName { get; protected set; } public virtual string Email { get; protected set; } public virtual string Name { get; set; } public virtual string Surname { get; set; } public virtual bool EmailConfirmed { get; protected set; } public virtual string PhoneNumber { get; protected set; } public virtual bool PhoneNumberConfirmed { get; protected set; } protected AppUser() { } public AppUser(IUserData user) : base(user.Id) { TenantId = user.TenantId; UpdateInternal(user); } public virtual bool Update(IUserData user) { if (Id != user.Id) { throw new ArgumentException($"Given User's Id '{user.Id}' does not match to this User's Id '{Id}'"); } if (TenantId != user.TenantId) { throw new ArgumentException($"Given User's TenantId '{user.TenantId}' does not match to this User's TenantId '{TenantId}'"); } if (Equals(user)) { return false; } UpdateInternal(user); return true; } protected virtual bool Equals(IUserData user) { return Id == user.Id && TenantId == user.TenantId && UserName == user.UserName && Name == user.Name && Surname == user.Surname && Email == user.Email && EmailConfirmed == user.EmailConfirmed && PhoneNumber == user.PhoneNumber && PhoneNumberConfirmed == user.PhoneNumberConfirmed; } protected virtual void UpdateInternal(IUserData user) { Email = user.Email; Name = user.Name; Surname = user.Surname; EmailConfirmed = user.EmailConfirmed; PhoneNumber = user.PhoneNumber; PhoneNumberConfirmed = user.PhoneNumberConfirmed; UserName = user.UserName; } }
另外我們還需要參照創建一個AppUserLookupService來快捷獲取用戶的對象信息。只需要繼承自UserLookupService即可,如下代碼所示,放在領域層中。
public class AppUserLookupService : UserLookupService<AppUser, IAppUserRepository>, IAppUserLookupService { public AppUserLookupService( IAppUserRepository userRepository, IUnitOfWorkManager unitOfWorkManager) : base( userRepository, unitOfWorkManager) { } protected override AppUser CreateUser(IUserData externalUser) { return new AppUser(externalUser); } }
這樣就可以在需要的時候(一般在AppService應用服務層中注入IAppUserLookupService),可以利用這個接口獲取對應的用戶信息,來實現相關的用戶關聯操作。
2、領域對象的關系處理
在常規的崗位領域對象中,增加一個和中間表的關系信息。
這個中間表的領域對象如下所示。
/// <summary> /// 崗位用戶中間表對象,領域對象 /// </summary> [Table("TB_JobPostUser")] public class JobPostUser : CreationAuditedEntity, IMultiTenant { /// <summary> /// 默認構造函數(需要初始化屬性的在此處理) /// </summary> public JobPostUser() { } /// <summary> /// 參數化構造函數 /// </summary> /// <param name="postId"></param> /// <param name="userId"></param> /// <param name="tenantId"></param> public JobPostUser(string postId, Guid userId, Guid? tenantId = null) { PostId = postId; UserId = userId; TenantId = tenantId; } /// <summary> /// 復合鍵的處理 /// </summary> /// <returns></returns> public override object[] GetKeys() { return new object[] { PostId, UserId }; } #region Property Members [Required] public virtual string PostId { get; set; } [Required] public virtual Guid UserId { get; set; } /// <summary> /// 租戶ID /// </summary> public virtual Guid? TenantId { get; protected set; } #endregion }
這里主要就是注意復合鍵的處理,其他的都是代碼自動生成的(利用代碼生成工具Database2Sharp)
然后在EntityFramework項目中處理它們之間的關系,如下代碼所示
public static class FrameworkDbContextModelBuilderExtensions { public static void ConfigureFramework( [NotNull] this ModelBuilder builder) { Check.NotNull(builder, nameof(builder)); builder.Entity<JobPost>(b => { b.ConfigureByConvention(); b.HasMany(x => x.Users).WithOne().HasForeignKey(jp => jp.PostId); b.ApplyObjectExtensionMappings(); }); builder.Entity<JobPostUser>(b => { b.ConfigureByConvention(); b.HasKey(pu => new { pu.PostId, pu.UserId }); b.HasIndex(pu => new { pu.PostId, pu.UserId }); b.ApplyObjectExtensionMappings(); }); builder.TryConfigureObjectExtensions<FrameworkDbContext>(); } }
通過JobPost關系中的HasForeignKey(jp => jp.PostId),建立它們的外鍵關系,通過JobPostUser關系中 b.HasKey(pu => new { pu.PostId, pu.UserId });創建中間表的復合鍵關系。
默認在獲取實體類的時候,關聯信息是沒有加載的,我們可以通過設置的方式實現預先加載或者懶加載處理,如下是通過設置,可以設置JobPost中加載用戶信息。
不過不是所有的實體信息,都是要設置這樣,否則有性能問題的。
最后測試的時候,可以看到返回的JobPost領域對象中附帶有用戶相關的信息,如下截圖所示。
這樣我們就可以通過該對象獲取用戶的相關信息,來進行相關的處理。
我們領域對象JobPost里面有Users屬性,它是一個中間表的信息,
而我們在Dto層,一般直接面向的是用戶信息,那么JobPostDto的信息定義如下所示。
那么我們在映射的時候,需要注意他們類型不一致的問題,需要忽略它的這個屬性的映射。
/// <summary> /// JobPost,映射文件 /// 注:一個業務對象拆分為一個映射文件,方便管理。 /// </summary> public class JobPostMapProfile : Profile { public JobPostMapProfile() { CreateMap<JobPostDto, JobPost>(); CreateMap<JobPost, JobPostDto>().Ignore(x => x.Users); //忽略Users,否則類型不對出錯 CreateMap<CreateJobPostDto, JobPost>(); } }
這樣就可以順利轉換獲得對應的信息。