Repository 仓储层
Repository仓储层属于数据操作层,理论只应该包含最基本的数据库操作和基本的缓存操作。不应该包含任何业务判断。
目前仓储层的具体实现基于Freesql,更多细节请参考Freesql文档
文件夹结构
- Entites 表实体
- View 视图实体
- MappingDatas 多表结果存放,常见于Join多表后的数据
- Repositories 仓储类
各个类型的具体定义
Entity
Entity就是最基本的数据库表,命名规范就是 大驼峰的表名+Entity,就像是下面的AccountInfoEntity
。
使用[Table]
标记这个类是一个数据库表类
///<summary>
///账户信息表
///</summary>
[Table(Name = "AccountInfo")]
public class AccountInfoEntity
{
///<summary>
///主键
///</summary>
[Column(IsPrimary = true)]
[Description("主键")]
public long AccountInfoSeq { get; set; }
///<summary>
///账户ID
///</summary>
[Description("账户ID")]
public string UserId { get; set; }
///<summary>
///工号
///</summary>
[Description("工号")]
public string UserCode { get; set; }
///<summary>
///姓名
///</summary>
[Description("姓名")]
public string UserName { get; set; }
/// <summary>
/// 人员类型
/// </summary>
[Description("人员类型")]
public UserTypeEnum UserType { get; set; }
...
}
Column 属性
Column属性能提供很多功能,常用的主要是以下几种。
1.指定主键
[Column(IsPrimary = true)]
[Description("主键")]
public long AccountInfoSeq { get; set; }
2.指定该键不可更新(CanInsert = false
)、不可插入(CanUpdate = false)
,默认是可更新,可插入的。常见于CreateTime/UpdateTime等属性
[Column(CanUpdate = false)]
[Description("创建时间")]
public DateTime CreateTime { get; set; }
3.指定该键为数据库时间ServerTime
,指定了该属性后,在创建/更新时不需要再指定时间,标记在Insert/Update
时会获取getdate()
作为该字段的值,更为精准。推荐在CreateTime/UpdateTime上使用
- 搭配`CanUpdate可以实现只在创建时使用的效果,如同下面的CreateTime。
- DateTimeKind 枚举提供多种选项,最常用的是
Utc
和Local
,Utc是格林威治时间,这里还是更推荐使用Local本地时间
[Column(ServerTime = DateTimeKind.Local, CanUpdate = false)]
public DateTime CreateTime { get; set; }
[Column(ServerTime = DateTimeKind.Local)]
public DateTime UpdateTime { get; set; }
4.自定义插入值,常见于插入默认值,例如IsDeleted,SEQUENCE序列使用等。
[Column(InsertValueSql = "'0'")]
public bool IsDeleted { get; set; }
在使用好Column列属性后,在做仓储层操作的时候就可以省略很多操作,可以降低错误率
CreateTime
、UpdateTime
、IsDeleted
等属性均无需再手动赋值。- 请确保所有表都包含
IsDeleted
,在Aegis框架中提供了对数据库软删除的过滤器,在使用仓储层查询时不会查询到IsDeleted = true
的数据。 - 在
1.04
版本的Aegis.Core.Freesql
包里包含DisableDeleted
方法,可以用于关闭软删除过滤,结合using使用的话,就可以暂时关闭软删除功能。public class CodeRecordRepository : BaseRepository<CodeRecordEntity, long> { public List<CodeRecordEntity> GetAllData(string templateId) { //将暂时关闭软删除过滤,请一定要结合using使用,否则并不是暂时关闭,而是永久关闭。 using(this.DisableDeleted()) { return this.Where(x => x.TemplateId == templateId).ToList(); } } }
View
View与Entity基本属于同等类实体,只是在放在View文件夹中,为了跟常规表实体作区分。
MappingData
MappingData与View类似,但主要是用于多表Join后的数据映射使用。
如同下方的DeviceManagerMappingData
public class DeviceManagerMappingData
{
public string DeviceSeq { get; set; }
public string DeviceName { get; set; }
public string HospitalCode { get; set; }
public string HospitalName { get; set; }
public string IpAddress { get; set; }
public string CreateTime { get; set; }
public string CreateUserName { get; set; }
public string UpdateTime { get; set; }
public string UpdateUserName {get;set;}
}
在Join后使用ToPagedList
为MappingData
public async Task<PagedList<DeviceManagerMappingData>> GetDeviceManagerListByPage(QueryDeviceManagerReqDto query)
{
var list = await Db.Select<DeviceManagerEntity, HospitalMainEntity>().WithLock(SqlServerLock.NoLock)
.LeftJoin((a, b) => a.HospitalCode.Equals(b.HospitalCode))
.WhereIf(!string.IsNullOrWhiteSpace(query.Keyword), (a, b) => a.DeviceName.Contains(query.Keyword)
|| a.IpAddress.Contains(query.Keyword) || a.CreateUserCode.Contains(query.Keyword))
.WhereIf(!string.IsNullOrWhiteSpace(query.HospitalCode), (a, b) => a.HospitalCode.Contains(query.HospitalCode))
.OrderByPropertyName(query.Sort, query.IsAscending)
.ToPagedListAsync(w => new DeviceManagerMappingData(),query.PageIndex, query.PageSize);
return list;
}
在仓储层返回MappingData后也可以在Services层使用DtoPaged.Convert
转换为合适的Dto
数据
Repository 仓储实现
Db类
命名规范为XXXDb 该类需继承于IDbSource
并且在配置文件中为其配置连接字符串即可,在Aegis.Core.Freesql
之后提供的1.05
版本中会自动读取配置文件。
/// <summary>
/// 数据库连接
/// </summary>
public partial class HisDb : IDbSource
{
/// <summary>
/// 初始化
/// </summary>
/// <param name="freeSql"></param>
/// <param name="dBConfig"></param>
public HisDb(IFreeSql<HisDb> freeSql)
{
SqlClient = freeSql;
}
public IFreeSql SqlClient { get; }
}
Repository
定义完具体Db类之后,就可以开始实现仓储类了。
命名规范为XXXXRepository
,需要继承BaseRepository<TEntity,TIdType>
的抽象类。对应泛型需要传入对应实体类型和对应实体的Id类型。
/// <summary>
/// 基础编码仓储
/// </summary>
public class BasicCodeRepository : BaseRepository<BasicCodeEntity, long>
{
/// <summary>
/// FreeSql
/// </summary>
private readonly IFreeSql _db;
/// <summary>
/// 支付配置连接
/// </summary>
/// <param name="db"></param>
public BasicCodeRepository(IFreeSql<HisDb> fsql) : base(fsql, null, null)
{
_db = fsql;
}
}
仓储类原生已经实现了基本的增删改查,可以使用this
来快速调用相关方法,在仓储层类下使用this,就类似于_db.Select<当前实体>
,在做简单查询时还是建议优先使用仓储层,仓储层会缓存常用数据,保证最快速度。
public async Task<PatientInfoEntity> GetPatientInfo(string seq)
{
bool canParse = long.TryParse(seq, out long id);
return await this
.WhereIf(canParse, x => x.PatientSeq == id || x.OldPatientSeq == seq)
.WhereIf(!canParse,x => x.OldPatientSeq == seq)
.WithLock(SqlServerLock.NoLock)
.FirstAsync();
}
推荐单表的增删改查都使用仓储类本身,只有多表或者复杂查询才使用IFreesql实例。但是更推荐使用当前仓储类的Orm
而不是直接使用注入进来的IFreesql
,这样在使用外部事务的时候能正常处理。
//多表联查使用IFreesql实例
decimal total = await this.Orm.Select<IpVisitPrePayEntity, IpVisitEntity>().WithLock(SqlServerLock.NoLock)
.LeftJoin((a, b) => a.IpVisitSeq.Equals(b.VisitSeq))
.Where((a, b) => b.PMINo.Equals(ipVisit.PMINo) && a.PrePayStatus == PrePayStatusEnum.正常
&& a.SettledFlag == false)
.WhereIf(item.PrePayStatus > 0, (a, b) => a.PrePayStatus.Equals(item.PrePayStatus))
.SumAsync((a, b) => a.PrePayAmount);
仓储层的常见问题
需要注意的是,仓储层本身是不可以跨线程使用的;但是IFreesql的实例可以跨线程使用,本身是单例的。
错误示范
var ipVisitEntity = await _ipVisitRepository.Where(it => it.IpVisitNo.Equals(item.IpVisitNo) && stateArray.Contains((int)it.VisitStatusCode)).FirstAsync();
//这里的_ipVisitRepository会为null,不可使用
_ = Task.Run(async () => {
var babys = _ipVisitRepository.Where(x => x.IsBaby == true).ToList();
});
正确示范
var ipVisitEntity = await _ipVisitRepository.Where(it => it.IpVisitNo.Equals(item.IpVisitNo) && stateArray.Contains((int)it.VisitStatusCode)).FirstAsync();
_ = Task.Run(async () => {
var ipVisitRepository = new IpVisitRepository(_freeSql); //重新new出仓储类
var babys = _ipVisitRepository.Where(x => x.IsBaby == true).ToList();
});
PagedList
在Repository
层提供了PagedList
的结构来存储分页信息。
[Serializable]
[StructLayout(LayoutKind.Auto)]
public readonly struct PagedList<T> : IEquatable<PagedList<T>>
{
public List<T> Entities { get; init; }
public long Total { get; init; }
public int CurrentIndex { get; init; }
public int PageSize { get; init; }
public PagedList(List<T> entities, long total, int currentIndex, int pageSize)
{
Entities = entities;
Total = total;
CurrentIndex = currentIndex;
PageSize = pageSize;
}
ToPagedList
可以在Freesql的表达式后通过ToPagedList
或ToPagedListAsync
来快速分页。
需要注意的是,如果要获取固定顺序的列表,建议加上OrderBy
。这两个方法均可以快速获取PagedList。
PagedList<View_IpVisitEntity> = _db.Queryable<View_IpVisitEntity>().
.WhereIf(query.VisitStatusCode != null, it => it.VisitStatusCode.Equals(query.VisitStatusCode))
.OrderBy(x => x.IdentityNo)
.ToPagedListAsync(query.PageIndex, query.PageSize);
PagedList是一种不可变的结构体类型,常规情况下建议只作为返回值使用,尽量不要将其作为其他方法的入参。
同时,该分页还支持多表Join使用,最多支持到8个表。
对应的w可以支持w.t1,w.t2
以此类推到t8,就是按顺序对应的具体表,比如在下方的例子中,w.t1
就是BasicCodeEntity
,w.t2
就是BasicCodeCategoryEntity
var basicCodeList = await _db.Select<BasicCodeEntity, BasicCodeCategoryEntity>()
.InnerJoin((x, y) => y.InternalCode.Equals(x.Category) && !y.IsDeleted)
.WhereIf(!string.IsNullOrEmpty(request.Keyword), (x, y) => y.Keywords.Contains(request.Keyword))
.WhereIf(!string.IsNullOrWhiteSpace(request.CategoryCode),
(x, y) => x.Category.Equals(request.CategoryCode))
.Where((x, y) => x.IsDeleted == false)
.OrderBy((x, y) => x.DisplayOrder)
.ToPagedListAsync(w => new BasicMappingData(){ CategoryCode = w.t1.CategoryCode,Keyword = y.Keyword,... }, request.PageIndex, request.PageSize);
也可以利用自动匹配字段机制来完成
public async Task<PagedList<DeviceManagerMappingData>> GetDeviceManagerListByPage(DeviceManagerQuery query)
{
var list = await Db.Select<DeviceManagerEntity, HospitalMainEntity>().WithLock(SqlServerLock.NoLock)
.LeftJoin((a, b) => a.HospitalCode.Equals(b.HospitalCode))
.WhereIf(!string.IsNullOrWhiteSpace(query.Keyword), (a, b) => a.DeviceName.Contains(query.Keyword)
|| a.IpAddress.Contains(query.Keyword) || a.CreateUserCode.Contains(query.Keyword))
.WhereIf(!string.IsNullOrWhiteSpace(query.HospitalCode), (a, b) => a.HospitalCode.Contains(query.HospitalCode))
.OrderByPropertyName(query.Sort, query.IsAscending)
.ToPagedListAsync(w => new DeviceManagerMappingData(),query.PageIndex, query.PageSize);
return list;
}