Skip to content

如何使用仓储(三) CodeFirst

alpha.yu edited this page Jul 5, 2022 · 22 revisions

前言

    本文主要介绍在ADNC框架中实体如何映射到数据库,示例采用CodeFirst模式,当然你也可以使用DBFirst模式。 本文所有操作都是在Adnc.Cust微服务中完成,其它微服务定义方式都一样。

如何映射

第一步,创建实体

我们需要在Adnc.Cus.Repository工程下的Entities目录创建实体,如Customer(如果是DDD模式则是Adnc.Xxx.Domain工程)。

如果采用经典三层模式开发,我们定义的实体必须直接继承或间接继承EfEntity类。
如果采用DDD架构模式开发, 我们定义的实体必须直接继承或间接继承AggregateRoot或者DomainEntity
Adnc.Cust 是采用经典三层模式开发的,DDD架构模式请参考Adnc.Ord/Adnc.Whse。实现模式都一样。

EfEntity 经典三层开发模式中所有实体类的基类

//重要派生类
public abstract class EfBasicAuditEntity : EfEntity, IBasicAuditInfo
{
}
//重要派生类
public abstract class EfFullAuditEntity : EfEntity, IFullAuditInfo
{
}

创建一个实体类完整代码如下:

namespace Adnc.Cus.Entities
{
    /// <summary>
    /// 客户表
    /// </summary>
    public class Customer : EfFullAuditEntity
    {
        public string Account { get; set; }

        public string Nickname { get; set; }

        public string Realname { get; set; }

        public virtual CustomerFinance FinanceInfo { get; set; }

        public virtual ICollection<CustomerTransactionLog> TransactionLogs { get; set; }
    }
}

第二步,定义映射关系

Entities/Config目录下创建映射关系类,如CustomerConfig

通过fluentapi创建映射关系。

namespace Adnc.Cus.Entities.Config
{
    public class CustomerConfig : EntityTypeConfiguration<Customer>
    {
        public override void Configure(EntityTypeBuilder<Customer> builder)
        {
            base.Configure(builder);

            builder.HasOne(d => d.FinanceInfo).WithOne(p => p.Customer).HasForeignKey<CustomerFinance>(d => d.Id).OnDelete(DeleteBehavior.Cascade);

            builder.HasMany(d => d.TransactionLogs).WithOne().HasForeignKey(p => p.CustomerId).OnDelete(DeleteBehavior.Cascade);

            builder.Property(x => x.Account).IsRequired().HasMaxLength(CustConsts.Account_MaxLength);

            builder.Property(x => x.Nickname).IsRequired().HasMaxLength(CustConsts.Nickname_MaxLength);

            builder.Property(x => x.Realname).IsRequired().HasMaxLength(CustConsts.Realname_Maxlength);
        }
    }
}

很多示例中CustomerConfig是直接继承IEntityTypeConfiguration<TEntity>这个接口。我这里稍微封装了下。创建了一个EntityTypeConfiguration<TEntity>抽象类并实现了IEntityTypeConfiguration<TEntity>接口。然后我们实体关系映射类再继承这个抽象类。这样做主要是为了统一处理一些公共特性字段的映射。如软删除、并发列映射等等,代码如下。

public abstract class EntityTypeConfiguration<TEntity> : IEntityTypeConfiguration<TEntity>
   where TEntity : Entity
{
    public virtual void Configure(EntityTypeBuilder<TEntity> builder)
    {
        var entityType = typeof(TEntity);
        ConfigureKey(builder, entityType);
        ConfigureConcurrency(builder, entityType);
        ConfigureQueryFilter(builder, entityType);
    }

    protected virtual void ConfigureKey(EntityTypeBuilder<TEntity> builder, Type entityType)
    {
        builder.HasKey(x => x.Id);
        builder.Property(x => x.Id).HasColumnOrder(1).ValueGeneratedNever();
    }

    protected virtual void ConfigureConcurrency(EntityTypeBuilder<TEntity> builder, Type entityType)
    {
        if (typeof(IConcurrency).IsAssignableFrom(entityType))
            builder.Property("RowVersion").IsRequired().IsRowVersion().ValueGeneratedOnAddOrUpdate();
    }

    protected virtual void ConfigureQueryFilter(EntityTypeBuilder<TEntity> builder, Type entityType)
    {
        if (typeof(ISoftDelete).IsAssignableFrom(entityType))
        {
            builder.Property("IsDeleted")
                       .HasDefaultValue(false)
                       .HasColumnOrder(2);
            builder.HasQueryFilter(d => !EF.Property<bool>(d, "IsDeleted"));
        }
    }
}

第三步,创建EntityInfo类

Entities创建一个EntityInfo类,并实现IEntityInfo接口。这个类每个工程只需要定义一个,是公用的。我的项目模板生成工具写好后,项目模板生成工具生成的项目会包含这个类。GetEntitiesTypeInfo()方法就是在当前程序集中查找继承了EfEntity的类,并放入集合中。

namespace Adnc.Cus.Entities
{
    public class EntityInfo : AbstractEntityInfo
    {
        public override IEnumerable<EntityTypeInfo> GetEntitiesTypeInfo()
        {
            return base.GetEntityTypes(this.GetType().Assembly).Select(x => new EntityTypeInfo() { Type = x, DataSeeding = default });
        }
    }
}

第四步,注入EntityInfo到容器,services.AddEfCoreContextWithRepositories()扩展方法中会统一注册。

public abstract class AbstractApplicationDependencyRegistrar : IDependencyRegistrar
{
    protected virtual void AddEfCoreContextWithRepositories(Action<IServiceCollection> action = null)
    {
        action?.Invoke(Services);

        var serviceType = typeof(IEntityInfo);
        var implType = RepositoryOrDomainAssembly.ExportedTypes.FirstOrDefault(type => type.IsAssignableTo(serviceType) && type.IsNotAbstractClass(true));
        if (implType is null)
            throw new NullReferenceException(nameof(IEntityInfo));
        else
            Services.AddSingleton(serviceType, implType);
        //注册其他服务
    }
}

第五步,生成迁移代码并更新到数据库

  • 设置Adnc.Cus.WebApi为启动项目(迁移命令会从这个工程读取数据库连接串)
  • 在VS工具中打开Nuget的程序包管理器控制台(工具=>Nuget包管理器=>程序包管理器控制台)
  • 设置“程序包管理器控制台”默认项目为Adnc.Cus.Migrations
  • 执行命令add-migration Update2021030401。执行成功后,会在Adnc.Cus.Migrations工程的Migrations目录下生成迁移文件。
  • 执行命令update-database,更新到数据库。

实体是如何与数据库关联起来的呢?

我们看Adnc.Infra.EfCore.MySQL工程的AdncDbContext类的源码。

namespace Adnc.Infra.EfCore.MySQL
{
    public class AdncDbContext : DbContext
    {
        private readonly Operater _operater;
        private readonly IEntityInfo _entityInfo;
        private readonly UnitOfWorkStatus _unitOfWorkStatus;

        //构造函数注入IEntityInfo接口
        public AdncDbContext([NotNull] DbContextOptions options, Operater operater,IEntityInfo entityInfo, UnitOfWorkStatus unitOfWorkStatus)
            : base(options)
        {
            _operater = operater;
            _entityInfo = entityInfo;
            _unitOfWorkStatus = unitOfWorkStatus;
            Database.AutoTransactionsEnabled = false;
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.HasCharSet("utf8mb4 ");
            //添加实体到模型上下文
            var entityInfos = _entityInfo.GetEntitiesTypeInfo().ToList();
            Guard.Checker.NotNullOrAny(entityInfos, nameof(entityInfos));
            foreach (var info in entityInfos)
            {
                if (info.DataSeeding.IsNullOrEmpty())
                    modelBuilder.Entity(info.Type);
                else
                    modelBuilder.Entity(info.Type).HasData(info.DataSeeding);
            }
            
            //从程序集加载fuluentapi加载配置文件
            var assembly = entityInfos.FirstOrDefault().Type.Assembly;
            modelBuilder.ApplyConfigurationsFromAssembly(assembly);

            //这里做两件事情
            //1、统一把表名,列名转换成小写。
            //2、读取实体的注释<Summary>部分填充Comment
            var types = entityInfos.Select(x => x.Type);
            var entityTypes = modelBuilder.Model.GetEntityTypes().Where(x => types.Contains(x.ClrType)).ToList();
            entityTypes.ForEach(entityType =>
            {
                modelBuilder.Entity(entityType.Name, buider =>
                {
                    var typeSummary = entityType.ClrType.GetSummary();
                    buider.ToTable(entityType.ClrType.Name.ToLower()).HasComment(typeSummary);

                    var properties = entityType.GetProperties().ToList();
                    properties.ForEach(property =>
                    {
                        var memberSummary = entityType.ClrType.GetMember(property.Name).FirstOrDefault().GetSummary();
                        buider.Property(property.Name)
                            .HasColumnName(property.Name.ToLower())
                            .HasComment(memberSummary);
                    });
                });
            });
        }
    }
}

WELL DONE
全文完