Repository 仓储层

Repository仓储层属于数据操作层,理论只应该包含最基本的数据库操作和基本的缓存操作。不应该包含任何业务判断。

目前仓储层的具体实现基于Freesql,更多细节请参考Freesql文档

文件夹结构

450

  • 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 枚举提供多种选项,最常用的是UtcLocal,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列属性后,在做仓储层操作的时候就可以省略很多操作,可以降低错误率

  1. CreateTimeUpdateTimeIsDeleted等属性均无需再手动赋值。
  2. 请确保所有表都包含IsDeleted,在Aegis框架中提供了对数据库软删除的过滤器,在使用仓储层查询时不会查询到IsDeleted = true的数据。
  3. 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后使用ToPagedListMappingData

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的表达式后通过ToPagedListToPagedListAsync来快速分页。 需要注意的是,如果要获取固定顺序的列表,建议加上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就是BasicCodeEntityw.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;
}


results matching ""

    No results matching ""