- 嘗試新的開發組合:Asp.NET Core+ABP框架+IdentityServer4+MySQL+Ext JS
- Asp.NET Core+ABP框架+IdentityServer4+MySQL+Ext JS之配置IdentityServer
- Asp.NET Core+ABP框架+IdentityServer4+MySQL+Ext JS之數據遷移
- Asp.NET Core+ABP框架+IdentityServer4+MySQL+Ext JS之添加實體
在ABP框架中,實體類是在Core項目中定義的。根據模版提供的Core項目,可以看到,實體類都是根據功能劃分到不同的文件夾的。在這裏,我們可以將SimpleCMS都放到CMS文件夾內,也可以單獨方在獨立的文件夾內。在本練習將使用獨立文件夾的方式。
要定義實體,可以從Entity
、Entity<T&>
、IEntity
和IEntity<T>
等類或接口中派生。這4個類或接口中,Entity
派生於Entity<int>
、 IEntity
和IEntity<T>
,使用整型作爲實體的主鍵;Entity<T>
是接口IEntity<T>
的實現,也就是已經爲你實現了接口的功能,不再需要自己去實現接口功能。從這4個類或接口的定義來看,一般情況下,我們從Entity
類或Entity<T>
類派生實體類就行,如果有特殊需求,就從接口中派生。
在定義實體類時,還可以爲實體類添加以下常用接口用來實現一些常用功能:
IHasCreationTime
:爲實體添加CreationTime
屬性,用來記錄實體的創建時間IHasDeletionTime
:爲實體添加DeletionTime
屬性,用來記錄實體的刪除時間,這個只有在使用軟刪除的時候纔有效。如果不是使用軟刪除,記錄都刪除了,這個字段沒有任何意義。IHasModificationTime
:爲實體添加LastModificationTime
屬性,用來記錄實體的最後修改時間ICreationAudited
:在IHasCreationTime
的基礎上添加CreatorUserId
屬性,用來記錄創建實體的用戶的IdIDeletionAudited
:在IHasDeletionTime
的基礎上添加DeleterUserId
屬性,用來記錄刪除實體的用戶的IdIModificationAudited
:在IHasModificationTime
的基礎上添加LastModifierUserId
屬性,用來記錄最後修改實體的用戶的IdIAudited
:ICreationAudited
和IModificationAudited
的合體,主要用於非軟刪除的情景IFullAudited
:IAudited
和IDeletionAudited
的合體,主要用於軟刪除的情景ISoftDelete
:爲實體添加IsDeleted
屬性,用於判斷實體是否已經被刪除,主要用於軟刪除的情景IPassivable
:爲實體添加IsActive
屬性,用於判斷實體是否處於活躍狀態IMayHaveTenant
:爲實體添加TenantId
屬性,用於指定實體所屬的租戶。該屬性允許值爲null,也就是可以指定租戶,也可以不指定IMustHaveTenant
:該接口與IMayHaveTenant
接口的主要區別是,必須指定租戶IExtendableObject
:爲實體添加ExtensionData
屬性,用於存儲JSON格式的數據。在實體中可通過SetData
方法來設置存儲的數據,通過GetData
來獲取存儲的數據
瞭解了實體類的定義方式後,我們來編寫類別實體類,在Core項目下新建一個Categories文件夾,並添加一個名爲Category的類,具體定義如下:
[Table("AppCategories")]
public class Category :Entity<long>, IFullAudited, IMustHaveTenant
{
public const int MaxStringLength = 255;
public const int MaxContentLength = 4000;
public long? ParentId { get; set; }
[ForeignKey("ParentId")]
public virtual Category Parent { get; set; }
[Required]
[MaxLength(MaxStringLength)]
public string Title { get; set; }
[MaxLength(MaxStringLength)]
public string Image { get; set; }
[MaxLength(MaxContentLength)]
public string Content { get; set; }
[DefaultValue(0)]
public int SortOrder { get; set; }
public virtual ICollection<Category> SubCategories { get; set; }
public virtual ICollection<Content> Contents { get; set; }
public DateTime CreationTime { get; set; }
public DateTime? LastModificationTime { get; set; }
public DateTime? DeletionTime { get; set; }
public long? CreatorUserId { get; set; }
public long? LastModifierUserId { get; set; }
public long? DeleterUserId { get; set; }
public bool IsDeleted { get; set; }
public int TenantId { get; set; }
public Category()
{
CreationTime = Clock.Now;
SortOrder = 0;
}
}
在代碼中,使用了Table
特性將實體對應的表的名稱定義爲了AppCategories。在類中,還加入了IFullAudited
和IMustHaveTenant
接口,說明類別實體將採用完整的審計功能,使用軟刪除來實現刪除,而且必須爲它設置租戶。
在實體的構造函數中,將創建時間設置爲了當前時間。在這裏沒有使用DataTime
的Now
屬性是因爲用戶可能在不同的時區使用系統,爲了能很好的處理這個問題,ABP定義了自己的時間操作功能。如果不考慮時區問題,這裏可以換回DataTime
對象。
估計很多人都會覺得奇怪,爲什麼在定義字符串的最大長度時,都要在實體類內定義一個常量呢?這是因爲在使用AutoMap來實現DTO類的時候,還需要定義一次最大長度,如果直接使用數字,那麼,當需要修改字符串長度的時候,就需要修改2次了,而使用常量的方式,只需要修改一次就行了。
由於在MySQL中觸發器與SQL Server的表現有點不同,因而沒有定義HierarchyLevel和FullPath這兩個字段。
由於Entity Framework Core不支持使用Index
特性來聲明索引,只能使用Fluent API來創建索引。切換到EntityFrameworkCore項目,打開SimpleCmsWithAbpDbContext.cs文件,在類內先添加實體集,代碼如下:
public DbSet<Category> Categories { get; set; }
然後在OnModelCreating
方法的最底部,添加以下代碼創建索引:
modelBuilder.Entity<Category>().HasIndex(p => p.SortOrder);
至此,類別實體就已經定義完了,相當的簡單。下面來定義文章實體,具體代碼如下:
[Table("AppContents")]
public class Content : Entity<long>, IFullAudited, IMustHaveTenant
{
public const int MaxStringLength = 255;
public const int MaxSummaryLength = 500;
[Required]
[MaxLength(MaxStringLength)]
public string Title { get; set; }
[Required]
public long CategoryId { get; set; }
[ForeignKey("CategoryId")]
public virtual Category Category { get; set; }
[MaxLength(MaxStringLength)]
public string Image { get; set; }
[MaxLength(MaxSummaryLength)]
public string Summary { get; set; }
[Required]
[Column(TypeName = "text")]
public string Body { get; set; }
[Required]
[DefaultValue(0)]
public int Hits { get; set; }
[Required]
[DefaultValue(0)]
public int SortOrder { get; set; }
public virtual ICollection<ContentTag> ContentTags { get; set; }
public DateTime CreationTime { get; set; }
public DateTime? LastModificationTime { get; set; }
public DateTime? DeletionTime { get; set; }
public long? CreatorUserId { get; set; }
public long? LastModifierUserId { get; set; }
public long? DeleterUserId { get; set; }
public bool IsDeleted { get; set; }
public int TenantId { get; set; }
public Content()
{
CreationTime = Clock.Now;
Hits = 0;
SortOrder = 0;
}
}
在這裏需要注意的是數據庫的區別,由於MySQL的存儲超長字符的數據類型有text、mediumtext和longtext等,大家需要根據需要進行選擇。在這裏我覺得使用text就足夠了,它可以存儲65535個字符。如果認爲不足夠,可以修改爲longtext,當然,一勞永逸的方法就是無論什麼情況,都設置爲longtext。
文章實體創建後,別忘記在Context中添加實體集和索引。
由於Entity Framework Core不再支持自動創建多對多關係的關聯表,需要顯式定義關聯表,因而,我們需要在Contents文件夾下再創建一個名爲ContentTag的實體,作爲文章和標籤的關聯實體。對於ContentTag實體,很有意思,如果從Entity<T>
派生,那就會爲它添加一個主鍵,而不能使用文章的Id和標籤的Id來創建主鍵。爲這個問題,我特意搜索了一下,找到了《Excluding the default Id primary key from an Entity….》這個帖子。在帖子中,ABP官方的答覆是使用文章的Id和標籤的Id創建一個唯一索引,而不去管那個主鍵,因爲這個主鍵是人畜無害的,而且在刪除的時候可以使用這個主鍵去刪除實體,也挺方便的。不過,官方的答覆人員對於這樣的結構也有點不爽,進一步的方法是使用NotMapped
特性屏蔽Id字段,不寫到數據庫,但帶來的問題是,要使用Repository
來處理實體的CURD操作,沒有Id主鍵會出現問題。除非重寫Repository
類,不然解決不了這個問題,但在ABP官方文檔《Repositories 》的最佳實踐(Repository Best Practices)一節中,建議不要去自定義存儲,而且重寫存儲也確實是比較大的工程,因而,筆者的看法是,雖然這樣使用是醜陋了點,但有時候做開發只能這樣折衷一下。
定義好的ContentTag實體代碼如下:
[Table("AppContentTags")]
public class ContentTag:Entity<long>
{
[Required]
public long ContentId { get; set; }
[ForeignKey("ContentId")]
public virtual Content Content { get; set; }
[Required]
public long TagId { get; set; }
[ForeignKey("TagId")]
public virtual Tag Tag { get; set; }
}
定義好ContentTag實體 後,在OnModelCreating
方法中爲實體添加索引,代碼如下:
modelBuilder.Entity<ContentTag>().HasIndex(p => new {p.ContentId, p.TagId}).IsUnique();
下面來完成標籤實體,代碼如下:
[Table("AppTags")]
public class Tag: Entity<long>, IMustHaveTenant
{
public const int MaxNameLength = 50;
[Required]
[MaxLength(MaxNameLength)]
public string Name { get; set; }
public int TenantId { get; set; }
public virtual ICollection<ContentTag> ContentTags { get; set; }
}
在標籤實體中,沒有使用審計功能。
還要爲標籤的Name字段添加唯一索引,代碼如下:
modelBuilder.Entity<Tag>().HasIndex(p => p.Name).IsUnique();
接下來是媒體實體,代碼如下:
public class Media : Entity<long>, ICreationAudited, IDeletionAudited, IMustHaveTenant
{
public const int MaxFileNameLength = 32;
public const int MaxDescriptionLength = 255;
public const int MaxPathLength = 10;
[Required]
[MaxLength(MaxFileNameLength)]
public string Filename { get; set; }
[Required]
[MaxLength(MaxDescriptionLength)]
public string Description { get; set; }
[Required]
[MaxLength(MaxPathLength)]
public string Path { get; set; }
[Required]
[Range(0, 2)]
[DefaultValue(0)]
public MediaType Type { get; set; }
[Required]
[DefaultValue(0)]
public int Size { get; set; }
public DateTime CreationTime { get; set; }
public DateTime? DeletionTime { get; set; }
public long? CreatorUserId { get; set; }
public long? DeleterUserId { get; set; }
public bool IsDeleted { get; set; }
public int TenantId { get; set; }
public Media()
{
CreationTime = Clock.Now;
}
}
由於媒體沒有更新功能,因而不需要更新審計,不採用IFullAudited接口,直接使用ICreationAudited和IDeletionAudited接口。
在定義媒體類型的時候,使用了枚舉類型的數據,定義如下:
public enum MediaType: byte
{
Image = 0,
Audio = 1,
Video = 2
}
最後是用戶配置實體,代碼如下:
[Table("AppUserProfiles")]
public class UserProfile :Entity<long>
{
public const int MaxKeywordLength = 200;
public const int MaxValueLength = 1000;
[DefaultValue(1)]
public UserProfileType UserProfileType { get; set; }
public long UserId { get; set; }
[ForeignKey("UserId")]
public virtual User User { get; set; }
[Required]
[MaxLength(MaxKeywordLength)]
public string Keyword { get; set; }
[Required]
[MaxLength(MaxValueLength)]
public string Value { get; set; }
}
這裏使用了一個UserProfileType的枚舉,代碼如下:
public enum UserProfileType : byte
{
State = 1
}
在Context中添加全部實體集後,就可調用以下語句添加遷移文件了:
Add-Migration AddCmsTables -Context SimpleCmsWithAbpDbContext
生成遷移文件後,不要使用Update-Database來更新數據庫,使用Migrator項目來進行遷移,以便爲類別表加入未分類類別。
在Seed文件夾下新建一個名爲Cms的文件夾,然後參考TenantRoleAndUserBuilder.cs文件創建一個名爲DefaultCategoryBuilder的類,代碼如下:
public class DefaultCategoryBuilder
{
private readonly SimpleCmsWithAbpDbContext _context;
private readonly int _tenantId;
public DefaultCategoryBuilder(SimpleCmsWithAbpDbContext context, int tenantId)
{
_context = context;
_tenantId = tenantId;
}
public void Create()
{
CreateDefaultTenant();
}
private void CreateDefaultTenant()
{
// Default tenant
if(_context.Categories.Any(m=>m.Title.Equals("未分類", StringComparison.CurrentCulture))) return;
var category = new Category() {Title = "未分類", Content = "", SortOrder = 0,TenantId = _tenantId };
_context.Categories.Add(category);
_context.SaveChanges();
}
}
接下來在SeedHelper類中的SeedHostDb
方法的底部添加以下代碼創建DefaultCategoryBuilder的實例來添加未分類類別:
new DefaultCategoryBuilder(context,1).Create();
好了,現在將Migrator項目設置爲啓動項目,執行一次,就可在數據庫中看到本文創建的實體了,打開appcategories表會看到一條記錄。