在ABP框架中,实体类是在Core项目中定义的。根据模版提供的Core项目,可以看到,实体类都是根据功能划分到不同的文件夹的。在这里,我们可以将SimpleCMS都放到CMS文件夹内,也可以单独方在独立的文件夹内。在本练习将使用独立文件夹的方式。
要定义实体,可以从Entity
、Entity
、IEntity
和IEntity
等类或接口中派生。这4个类或接口中,Entity
派生于Entity
、 IEntity
和IEntity
,使用整型作为实体的主键;Entity
是接口IEntity
的实现,也就是已经为你实现了接口的功能,不再需要自己去实现接口功能。从这4个类或接口的定义来看,一般情况下,我们从Entity
类或Entity
类派生实体类就行,如果有特殊需求,就从接口中派生。
在定义实体类时,还可以为实体类添加以下常用接口用来实现一些常用功能:
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 SubCategories { get; set; }
public virtual ICollection 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 Categories { get; set; }
然后在OnModelCreating
方法的最底部,添加以下代码创建索引:
modelBuilder.Entity().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 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
派生,那就会为它添加一个主键,而不能使用文章的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().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 ContentTags { get; set; }
}
在标签实体中,没有使用审计功能。
还要为标签的Name字段添加唯一索引,代码如下:
modelBuilder.Entity().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表会看到一条记录。