跳到主要内容
版本:3.0.0

Repository 层

这一层负责什么

Repository 层负责数据访问,把所有数据库操作收在这一层里:

  • 查库放这里
  • 落库放这里
  • FreeSql 的使用也收在这里

业务规则别写在这里。

目录结构

Repository
├── AegisDb.cs // IDbSource 实现
├── Entities/ // 实体类
├── Repositories/ // 仓储类
└── MappingDatas/ // 多表查询映射对象(按需)

DbSource 定义

每个数据库对应一个 IDbSource 实现类,给数据库连接一个泛型标识:

using Aegis.Core.FreeSql;

namespace Aegis.Webapi.Repository;

public class AegisDb : IDbSource
{
public AegisDb(IFreeSql<AegisDb> sqlClient)
{
SqlClient = sqlClient;
}

public IFreeSql<AegisDb> SqlClient { get; }
}

这个类不需要写复杂逻辑。定义之后,IFreeSql<AegisDb> 就可以作为泛型参数在仓储中使用。

Startup.cs 中注册:

services.AddDbSource<AegisDb>(options =>
{
options.ConnectionString = ConfigManager.Get("PostgreConnection");
options.DataType = "PostgreSQL";
});

services.AddDbRepositories<AegisDb>();

注册细节和配置项说明见 FreeSql 数据访问

Entity 怎么写

实体是数据库表对应的 C# 类。

基本约定

  • 类名以 Entity 结尾
  • [Table] 指定表名
  • 主键用 [Column(IsPrimary = true)]
  • 属性用 get; set;(不是 init,因为 FreeSql 需要赋值)
[Table(Name = "User")]
public class UserEntity
{
[Column(IsPrimary = true)]
public long Id { get; set; }

public string Name { get; set; }
}

时间字段

// 创建时间:插入时自动赋值,更新时不覆盖
[Column(ServerTime = DateTimeKind.Local, CanUpdate = false)]
public DateTime CreateTime { get; set; }

// 更新时间:每次更新自动刷新
[Column(ServerTime = DateTimeKind.Local)]
public DateTime UpdateTime { get; set; }

默认值

// 插入时使用 SQL 表达式赋值
[Column(InsertValueSql = "'0'")]
public bool IsDeleted { get; set; }

软删除

实现 IDeletedEntity 接口,框架会自动过滤 Deleted == true 的数据:

public class UserEntity : IDeletedEntity
{
[Column(IsPrimary = true)]
public long Id { get; set; }

public string Name { get; set; }

public bool Deleted { get; set; }
}

Repository 怎么写

基本仓储

继承 BaseRepository<TEntity, TKey>,构造函数注入 IFreeSql<TDbSource>

public class UserRepository : BaseRepository<UserEntity, long>
{
public UserRepository(IFreeSql<AegisDb> fsql) : base(fsql)
{
}

public Task<UserEntity> GetByIdAsync(long id)
{
return this.Where(x => x.Id == id).FirstAsync();
}
}

单表查询

在 Repository 内使用 this 即可操作当前表:

public async Task<UserEntity> GetByNameAsync(string name)
{
return await this
.Where(x => x.Name == name)
.FirstAsync();
}

条件查询

使用 WhereIf 根据条件动态过滤:

public async Task<List<UserEntity>> Search(string keyword, int? status)
{
return await this
.WhereIf(!string.IsNullOrEmpty(keyword), x => x.Name.Contains(keyword))
.WhereIf(status.HasValue, x => x.Status == status)
.ToListAsync();
}

排序

分页查询一定要加 OrderBy,否则结果顺序不稳定:

return await this
.Where(x => x.Status == 1)
.OrderBy(x => x.CreateTime)
.ToListAsync();

读取锁提示

在 SQL Server 下使用 NoLock 避免读锁:

return await this
.WithLock(SqlServerLock.NoLock)
.Where(x => x.Code == code)
.FirstAsync();

多表联查

多表查询使用 this.Orm,不要使用外部注入的 IFreeSql

public async Task<decimal> GetTotalAmount(string orderNo)
{
return await this.Orm
.Select<OrderEntity, OrderItemEntity>()
.LeftJoin((order, item) => order.Id == item.OrderId)
.Where((order, item) => order.OrderNo == orderNo)
.SumAsync((order, item) => item.Amount);
}

多表联查的结果可以映射到 MappingData 类中,放在 Repository 项目的 MappingDatas/ 目录下。

最佳实践

用 this.Orm 而不是注入的 IFreeSql

在事务场景中,this.Orm 已经绑定了当前工作单元,外部注入的 IFreeSql 没有。使用外部实例的操作不会参与事务:

// 错误:_freeSql 不在事务内
_freeSql.Insert(new UserEntity()).ExecuteAffrows();

// 正确:this.Orm 已绑定事务
this.Orm.Insert(new UserEntity()).ExecuteAffrows();

Service 不直接碰 IFreeSql

  • Service 层注入具体仓储类
  • Repository 层处理数据库访问
  • Service 需要复杂数据操作时,在 Repository 中封装方法,而不是自己写 SQL

Entity 和 DTO 不要混用

Entity 是数据库表结构,DTO 是接口返回结构。两者字段不完全一致时用 Mapster 映射,不要直接把 Entity 返回给前端。

仓储方法保持原子性

每个 Repository 方法只做一件事。如果一次操作涉及多张表的写入,在 Service 层用事务编排多个 Repository,而不是在一个 Repository 里操作多张表。

边界与限制

  • AddDbRepositories<TDbSource>() 扫描的是 TDbSource 所在程序集,Entity、Repository、DbSource 要放在同一个项目中
  • BaseRepository 的泛型参数 TKey 要和 Entity 的主键类型一致
  • Repository 不要写业务逻辑,只做数据访问

配套阅读