ABP框架中使用了数据传输对象和实体的概念,而实体一般又是与数据库的表结构相对应的。在ABP框架中可以分别为数据传输对象DTO与实体Entity、实体Entity与数据库表Table建立映射关系,既可以减少一定的编码工作量,也能降低数据传输对象、实体与数据库表之间的耦合性。
一.对象映射–数据传输对象与实体之间的映射
在ABP框架中,数据传输对象(Data Transfer Objects)也就是DTO用于应用层和展现层的数据传输,也就是展现层和应用层的使用者看到的都是DTO,但同时应用层与基础设施层之间的交互都是以领域层中的实体Entity为桥梁进行的,因此必然存在着数据传输对象与实体之间的转换。
以应用层一个新增职员的方法为例,我们在应用服务接口中定义如下方法。
DetailEmployeeDto Create(CreateEmployeeDto input);
输入为CreateEmployeeDto新增数据传输对象,输出为DetailEmployeeDto详情数据传输对象。
/// <summary>
/// 新增数据传输对象
/// </summary>
public class CreateEmployeeDto
{
/// <summary>
/// 人员姓名
/// </summary>
public virtual string EmployeeName { get; set; }
/// <summary>
/// 性别
/// </summary>
public virtual int EmployeeSex { get; set; }
/// <summary>
/// 职位/头衔
/// </summary>
public virtual string EmployeeTitle { get; set; }
/// <summary>
/// 联系电话
/// </summary>
public virtual string EmployeePhone { get; set; }
}
/// <summary>
/// 详情数据传输对象
/// </summary>
public class DetailEmployeeDto : EntityDto<string>
{
/// <summary>
/// 人员姓名
/// </summary>
public virtual string EmployeeName { get; set; }
/// <summary>
/// 性别
/// </summary>
public virtual int EmployeeSex { get; set; }
/// <summary>
/// 职位/头衔
/// </summary>
public virtual string EmployeeTitle { get; set; }
/// <summary>
/// 联系电话
/// </summary>
public virtual string EmployeePhone { get; set; }
}
将接口实现后,就涉及到数据对象与实体之间的转换;
public DetailEmployeeDto Create(CreateEmployeeDto input)
{
Employee entity=new Employee()
{
Id=Guid.NewGuid().ToString(),
EmployeeName = input.EmployeeName,
EmployeeTitle = input.EmployeeTitle,
EmployeeSex = input.EmployeeSex,
EmployeePhone = input.EmployeePhone
};
var result = _repository.Insert(entity);
DetailEmployeeDto output=new DetailEmployeeDto()
{
Id=result.Id,
EmployeeName = result.EmployeeName,
EmployeeTitle = result.EmployeeTitle,
EmployeePhone = result.EmployeePhone,
EmployeeSex = result.EmployeeSex
};
return output;
}
其中实体Employee为
/// <summary>
/// 职员
/// </summary>
public class Employee : Entity<string>
{
/// <summary>
/// 人员姓名
/// </summary>
public virtual string EmployeeName { get; set; }
/// <summary>
/// 性别
/// </summary>
public virtual int EmployeeSex { get; set; }
/// <summary>
/// 职位/头衔
/// </summary>
public virtual string EmployeeTitle { get; set; }
/// <summary>
/// 联系电话
/// </summary>
public virtual string EmployeePhone { get; set; }
}
可以看到在这个过程中需要将CreateEmployeeDto转换为Employee,再将Employee转换为DetailEmployeeDto。如果每个方法都这样进行手动赋值,恐怕手敲断了项目也完成不了。好在ABP框架提供了对象自动映射的组件,能够大大简化这一过程。
1.1.集成AutoMapper
ABP框架集成了AutoMapper组件,从字面意思上就可以看出来是自动映射的。Abp.AutoMapper提供了两种代码写法来实现对象映射。
1.1.1. 属性标记形式
在数据传输对象DTO或实体Entity的头部可以添加属性标记对映射关系进行定义。常见的有3种属性:
- AutoMapTo(A):将对象B转换为对象A
- AutoMapFrom(A):将对象A转换为对象B
- AutoMap:对象A、B可以互相转换
[AutoMapTo(typeof(Employee))]
public class CreateEmployeeDto
{
///
}
[AutoMapFrom(typeof(Employee))]
public class DetailEmployeeDto : EntityDto<string>
{
///
}
1.1.2. 单独定义形式
除了属性标记的形式外,也可以单独创建类对映射关系进行管理,同样也是来源于AutoMapper的用法。
public class EmployeeMapper
{
/// <summary>
/// 数据传输对象映射
/// </summary>
public static void CreateMapper(IMapperConfigurationExpression mapper)
{
mapper.CreateMap<CreateEmployeeDto, Employee>();
mapper.CreateMap<Employee, DetailEmployeeDto>();
}
}
同时在模块初始化时载入上述映射关系。
public override void Initialize()
{
IocManager.RegisterAssemblyByConvention(typeof(MyApplicationModule).GetAssembly());
//对象映射
Configuration.Modules.AbpAutoMapper().Configurators.Add(EmployeeMapper.CreateMapper);
}
这样做的好处是将对象映射关系统一管理,后面增加数据传输对象时也不用来回翻代码,而且可以利用AutoMapper提供的方法进行更加复杂的配置。
mapper.CreateMap<Source, Target>()
.ForMember(d => d.TargetName, o => o.MapFrom(s => s.SourceName))//特定属性映射
.ForMember(d => d.TargetNum, o => o.MapFrom((s => s.SourceNum > 20)))//属性过滤映射
.ForMember(d => d.Id, o => o.Ignore());//忽略映射
1.1.3. 对象映射的使用
通过对象的映射可以将前面的代码简化为:
public DetailEmployeeDto Create(CreateEmployeeDto input)
{
Employee entity = input.MapTo<Employee>();
var result = _repository.Insert(entity);
DetailEmployeeDto output = result.MapTo<DetailEmployeeDto>();
return output;
}
可以看到相当一部分的代码都被缩减了。
1.2.IObjectMapper接口
上面提到的AutoMap属性及MapTo扩展方法都来自于AutoMapper,ABP框架又做了进一步的封装,主要功能并没有变化。而ABP还提供了IObjectMapper接口,通过依赖注入引入到应用服务中,使用其Map方法同样能实现对象之间的映射。
public class EmployeeAppService : ApplicationService
{
private readonly IRepository<Employee,string> _employeeRepository;
private readonly IObjectMapper _objectMapper;
public EmployeeAppService(IRepository<Employee,string> employeeRepository, IObjectMapper objectMapper)
{
_employeeRepository = employeeRepository;
_objectMapper = objectMapper;
}
public DetailEmployeeDto Create(CreateEmployeeDto input)
{
Employee entity = _objectMapper.Map<Employee>(input);
var result = _repository.Insert(entity);
DetailEmployeeDto output = _objectMapper.Map<DetailEmployeeDto>(result);
return output;
}
}
二.ORM映射–实体与数据库表之间的映射
上一部分中数据传输对象与实体之间的映射本质上就类对象之间的转换,而这一部分中实体与数据表之间的映射则是截然不同的东西。只是从一个更抽象的层面考虑两部分都是一种对应关系的描述,所以勉强可以放在一起。
2.1 EFCore属性映射的基本用法
这里就以EntityFrameworkCore为例。下面就是一个简单的例子,可以通过实现IEntityTypeConfiguration接口对实体所对应的数据集表名、列名、数据类型、长度、默认值进行设置。此外也可以为数据库表添加初始化数据,以确保CodeFirst模式下数据库有初始化数据。
/// <summary>
/// Employee实体映射
/// </summary>
public class EmployeeMap : IEntityTypeConfiguration<Employee>
{
public void Configure(EntityTypeBuilder<Employee> builder)
{
builder.ToTable("table_employee");
builder.HasKey(m => m.Id);//主键
builder.Property(m => m.Id).HasColumnName("RowGuid");//列名
builder.Property(m => m.EmployeeName).HasMaxLength(50);
builder.Property(m => m.EmployeeSex).HasMaxLength(10);
builder.Property(m => m.EmployeeTitle).HasMaxLength(50).HasDefaultValue("职员");
builder.Property(m => m.EmployeePhone).HasMaxLength(30);
#region 初始化数据
List<Employee> employees=new List<Employee>();
employees.Add(new Employee(){Id="p1",EmployeeName = "小红", EmployeePhone = "13333333333",EmployeeTitle = "经理"});
employees.Add(new Employee() { Id = "p2", EmployeeName = "小明", EmployeePhone = "13333333334", EmployeeTitle = "主管", });
employees.Add(new Employee() { Id = "p3", EmployeeName = "小刚", EmployeePhone = "13333333335", EmployeeTitle = "组长" });
builder.HasData(employees.ToArray());
#endregion
}
}
以上的属性映射关系最终都应包含在DbContext中的OnModelCreating方法中才能有效。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new EmployeeMap());
base.OnModelCreating(modelBuilder);
}
2.2 EFCore属性映射的更多用法
- 设置表名
builder.ToTable(“table_name”);
- 设置主键
builder.HasKey(m => m.Id);
- 设置联合主键
builder.HasKey(t =>new{t.EmployeeName,t.Id} );
- 取消数据库字段标识(取消自动增长)
builder.Property(t=>t.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
- 设置数据库字段标识(自动增长)
builder.Property(t =>t.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
- 设置字段最大长度
builder.Property(t => t.EmployeeName).HasMaxLength(50);
- 设置字段为必需
builder.Property(t =>t.Id).IsRequired();
- 属性不映射到数据库
builder.Ignore(t => t.EmployeeName);
- 将属性指定数据库列名:
builder .Property(t => t.EmployeeName) .HasColumnName(“EmployeeName”);
- 级联删除(数据库默认是不级联删除的)
builder.HasRequired(t => t.Company).WithMany(t => t.Employee).HasForeignKey(d => d.CompanyId).WillCascadeOnDelete();
- 设置为Timestamp
builder.Property(t => t.Timestamp) .IsRowVersion();
- 1对0关系…(实体A可以包含零个或一个实体B)
builder.HasRequired(t => t.Instructor).WithOptional(t => t.OfficeAssignment);
- 1对1关系
builder.HasRequired(t => t.OfficeAssignment).WithRequiredPrincipal(t => t.Instructor);
- 1对多关系
builder.HasRequired(c => c.Company) .WithMany(t => t.Employee)
- 指定外键名(指定表Staff中的字段DepartmentID为外键)
builder .HasRequired(c => c.Company) .WithMany(t => t.Employee) .Map(m => m.MapKey(“CompanyId”));
- 多对多关系
builder.HasMany(t => t.Instructors).WithMany(t => t.Courses)
- 多对多关系并指定连接表名及列名
builder.HasMany(t => t.Instructors)
.WithMany(t => t.Courses)
.Map(m =>
{
m.ToTable(“CourseInstructor”);
m.MapLEFtKey(“CourseID”);
m.MapRightKey(“InstructorID”);
});
如果使用NHibernate或则其他ORM框架,可能就是另一种写法了,但基本思路是不变的。
通过对以上两部分映射关系的合理运用,能将项目代码变得更加灵活,对于开发效率的提升也是很有帮助的。