数据库
用 EF Core/Dapper…
适用于使用 EF Core/Dapper 设计数据访问层、优化慢数据库查询、选择读写访问方式并避免常见性能陷阱。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
用 EF Core/Dapper 设计数特定
解决思路
- 把读模型和写模型分离:不要为读写复用同一个 User 实体;读模型反规范化、面向查询效率并返回多种投影类型(如 UserProfile、UserSummary、UserDetailForAdmin);写模型规范化、面向校验并接收强类型命令(如 CreateUserCommand、UpdateUserCommand)。
- 按读写分离组织代码结构:在 MyApp.Data/Users 下放置读侧 IUserReadStore.cs、PostgresUserReadStore.cs,写侧 IUserWriteStore.cs、PostgresUserWriteStore.cs,读 DTO UserProfile.cs、UserSummary.cs,写命令 CreateUserCommand.cs、UpdateUserCommand.cs。
- 读存储接口返回多个专用投影而不是实体:GetByIdAsync 返回 UserProfile,GetByEmailAsync 返回 UserProfile,GetAllAsync 返回带 limit 和 cursor 的 UserSummary 列表,EmailExistsAsync 返回 bool。
- 写存储接口接收强类型命令并最小化返回值:CreateAsync 只返回 UserId,UpdateAsync 返回 void 或抛错,DeleteAsync 显式删除。
- 保持读写结构差异:读侧返回多个 DTO 或 bool;写侧只返回最小数据;读查询用无状态投影且不需要跟踪;写操作聚焦命令校验;读库和写库可以使用不同数据库或表并接受最终一致性。
- 每个读方法都必须有可配置的行限制:接口中把 limit 作为必填参数,不要返回无界结果集,例如 IOrderReadStore.GetByCustomerAsync(CustomerId customerId, int limit, OrderId? cursor, CancellationToken ct)。
- 在 Dapper 查询中用 SQL LIMIT 实现行限制和游标分页:SELECT 指定列 FROM orders WHERE customer_id = @CustomerId AND (@Cursor IS NULL OR created_at < (SELECT created_at FROM orders WHERE id = @Cursor)) ORDER BY created_at DESC LIMIT @Limit。
- 在 EF Core 分页中始终 Take(paginator.PageSize):先 CountAsync 统计总数,再 Skip((paginator.PageNumber - 1) * paginator.PageSize).Take(paginator.PageSize),投影到 OrderSummary,再返回 PaginatedList。
- 对只读查询使用 AsNoTracking():例如 _context.Users.AsNoTracking().Where(u => u.IsActive).ToListAsync(),不要跟踪后续不会修改的实体。
- 读多写少的应用可在 DbContext 的 OnConfiguring 中设置 optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking),需要修改实体时再显式使用 AsTracking()。
- 避免 N+1 查询:不要先取订单列表再在 foreach 中逐条查询 OrderItems。
- 解决 N+1 的 EF Core 方案:使用 Include(o => o.Items) 一次查询取出关联集合。
- 解决 N+1 的 Dapper 方案:使用 QueryMultipleAsync 一次发送两条 SQL(orders 和 order_items),再用已取回的数据在内存中按 OrderId 拼接,避免逐条查询。
- 永不在应用侧做 join:不要分别查询 customers 和 orders 后在 C# 内存中用 Where/Select 连接,因为会产生 O(n*m) 内存开销。
- 把 join 放到 SQL 中:EF Core 用 Include(c => c.Orders);Dapper 写 LEFT JOIN orders o ON c.id = o.customer_id 并按需 GROUP BY。
- 避免多个 Include 造成笛卡尔爆炸:例如 Products 同时 Include Reviews、Images、Categories 可能产生 100*20*5=10000 行。
- 用 AsSplitQuery() 拆分多个 Include 查询:Products.AsSplitQuery().Include(p => p.Reviews).Include(p => p.Images).Include(p => p.Categories).FirstOrDefaultAsync(...),改为多次查询避免笛卡尔积。
- 优先用显式投影只取所需数据:ProductDetail 查询只 Select 需要的字段,并限制子集合,如 p.Reviews.OrderByDescending(r => r.CreatedAt).Take(10).ToList()、p.Images.Take(5).ToList()、p.Categories.Select(c => c.Name).ToList()。
- 在 EF Core 模型中约束列大小:用 HasMaxLength 配置最大值,例如 Email 254、Name 100、Bio 500;真正的大内容显式用 HasColumnType("text")。
- 不要构建通用仓储 IRepository<T>:避免 GetAllAsync 无限制、FindAsync(Expression<Func<T, bool>>) 难优化、隐藏 N+1、鼓励取过多数据。
- 使用目的明确的读存储:例如 IOrderReadStore 提供 GetByIdAsync、带 limit 的 GetByCustomerAsync、GetPendingAsync。
- 读密集或复杂读查询使用 Dapper 和显式 SQL:在 PostgresUserReadStore 中打开连接,查询 SELECT id, email, name, bio, created_at FROM users WHERE id = @Id,映射到内部 row 类型再转 UserProfile。
- 按场景选择 EF Core 或 Dapper:简单 CRUD 用 EF Core;复杂读查询用 Dapper;带校验写入用 EF Core;批量操作用 Dapper 或 raw SQL;报表/分析用 Dapper;领域重写入用 EF Core;同一项目可 EF Core 写、Dapper 读。
- 按反模式检查表逐项修复:无行限制就加 limit 参数;SELECT * 就只投影所需列;N+1 就用 Include 或批量查询;应用侧 join 就改为 SQL join;笛卡尔爆炸就用 AsSplitQuery 或投影;跟踪只读数据就用 AsNoTracking;通用仓储就改为专用读写存储;无界字符串就配置 MaxLength。
适用边界 · 注意事项
- 不适用于非关系型数据库或不使用 SQL join、LIMIT、EF Core/Dapper 的场景,因为核心做法依赖 SQL 和这些 .NET 数据访问工具。
- 当读模型与写模型必须强一致、不能接受最终一致性或读写分离带来的同步延迟时,不要把读写库分离作为默认方案。
- 简单一次性脚本或极简 CRUD 不值得引入完整 CQRS 读写存储结构,按复杂度选择即可。