Asp.NET Core+ABP框架+IdentityServer4+MySQL+Ext JS之添加实体

  1. 尝试新的开发组合:Asp.NET Core+ABP框架+IdentityServer4+MySQL+Ext JS
  2. Asp.NET Core+ABP框架+IdentityServer4+MySQL+Ext JS之配置IdentityServer
  3. Asp.NET Core+ABP框架+IdentityServer4+MySQL+Ext JS之数据迁移
  4. Asp.NET Core+ABP框架+IdentityServer4+MySQL+Ext JS之添加实体

在ABP框架中,实体类是在Core项目中定义的。根据模版提供的Core项目,可以看到,实体类都是根据功能划分到不同的文件夹的。在这里,我们可以将SimpleCMS都放到CMS文件夹内,也可以单独方在独立的文件夹内。在本练习将使用独立文件夹的方式。

要定义实体,可以从EntityEntity<T&>IEntity和IEntity<T>等类或接口中派生。这4个类或接口中,Entity派生于Entity<int>IEntityIEntity<T>,使用整型作为实体的主键;Entity<T>是接口IEntity<T>的实现,也就是已经为你实现了接口的功能,不再需要自己去实现接口功能。从这4个类或接口的定义来看,一般情况下,我们从Entity类或Entity<T>类派生实体类就行,如果有特殊需求,就从接口中派生。

在定义实体类时,还可以为实体类添加以下常用接口用来实现一些常用功能:

  • IHasCreationTime:为实体添加CreationTime属性,用来记录实体的创建时间
  • IHasDeletionTime:为实体添加DeletionTime属性,用来记录实体的删除时间,这个只有在使用软删除的时候才有效。如果不是使用软删除,记录都删除了,这个字段没有任何意义。
  • IHasModificationTime:为实体添加LastModificationTime属性,用来记录实体的最后修改时间
  • ICreationAudited :在IHasCreationTime的基础上添加CreatorUserId属性,用来记录创建实体的用户的Id
  • IDeletionAudited:在IHasDeletionTime的基础上添加DeleterUserId属性,用来记录删除实体的用户的Id
  • IModificationAudited:在IHasModificationTime的基础上添加LastModifierUserId属性,用来记录最后修改实体的用户的Id
  • IAuditedICreationAuditedIModificationAudited的合体,主要用于非软删除的情景
  • IFullAuditedIAuditedIDeletionAudited的合体,主要用于软删除的情景
  • 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。在类中,还加入了IFullAuditedIMustHaveTenant接口,说明类别实体将采用完整的审计功能,使用软删除来实现删除,而且必须为它设置租户。

在实体的构造函数中,将创建时间设置为了当前时间。在这里没有使用DataTimeNow属性是因为用户可能在不同的时区使用系统,为了能很好的处理这个问题,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表会看到一条记录。

相关文章
相关标签/搜索