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 不要写业务逻辑,只做数据访问
配套阅读
- FreeSql 数据访问 — 注册 API 和配置项说明
- 事务 — UnitOfWork 和事务最佳实践
- 分页与列表数据 — 分页查询和 Dto 转换
- 对象映射 — Entity 和 Dto 之间的映射