# 数据访问 (02_Data)

最后更新: 2024-09-20


# 📚 概述

DarkM 数据访问层是基于 Dapper 封装的轻量级 ORM,支持 SqlServer、MySql、SQLite、PostgreSQL、Oracle 五种数据库,提供完整的 CRUD、批量操作、分表操作、工作单元等功能。

源代码位置: DarkM/src/Framework/Data


# 🏗️ 模块架构

# 完整目录结构

Data/
├── Core/                                    # 核心层
│   └── Data.Abstractions/                   # 抽象层
│       ├── IDbContext.cs                    # 数据库上下文接口
│       ├── IDbSet.cs                        # 数据集接口
│       ├── IRepository.cs                   # 仓储接口
│       ├── IEntity.cs                       # 实体接口
│       ├── IUnitOfWork.cs                   # 工作单元接口
│       ├── Options/
│       │   ├── DbOptions.cs                 # 数据库配置
│       │   └── DbModuleOptions.cs           # 模块配置
│       ├── Pagination/
│       │   ├── Paging.cs                    # 分页模型
│       │   └── Sort.cs                      # 排序模型
│       ├── Attributes/
│       │   ├── TableAttribute.cs            # 表特性
│       │   ├── ColumnAttribute.cs           # 列特性
│       │   └── KeyAttribute.cs              # 主键特性
│       └── Enums/
│           └── SqlDialect.cs                # 数据库类型
│
├── Db/                                      # 数据库驱动
│   ├── Data.SqlServer/
│   ├── Data.MySql/
│   ├── Data.SQLite/
│   ├── Data.PostgreSQL/
│   └── Data.Oracle/
│
└── Extend/                                  # 扩展层
    ├── Data.Query/                          # 查询扩展
    │   ├── QueryModel.cs                    # 查询模型
    │   └── QueryPagingModel.cs              # 分页查询模型
    └── Data.Integration/                    # 集成层
        └── ServiceCollectionExtensions.cs   # DI 扩展
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 🔧 支持的数据库

数据库 Dialect 值 默认端口 说明
SqlServer 0 1433 功能强大,Windows 友好
MySql 1 3306 开源流行,性能好
SQLite 2 - 嵌入式,零配置
PostgreSQL 3 5432 功能强大,开源
Oracle 4 1521 企业级,稳定

# 💡 配置说明

# appsettings.json 配置

{
  "Db": {
    // 是否开启 SQL 日志(开发环境建议开启)
    "Logging": false,
    
    // 数据库类型:0=SqlServer, 1=MySql, 2=SQLite, 3=PostgreSQL, 4=Oracle
    "Dialect": 2,
    
    // 数据库地址(SQLite 为文件路径)
    "Server": "../../data/SQLite",
    
    // 端口号
    "Port": 0,
    
    // 用户名
    "UserId": "",
    
    // 密码
    "Password": "",
    
    // 是否自动创建数据库和表
    "CreateDatabase": true,
    
    // 是否自动初始化数据
    "InitData": true,
    
    // 模块列表
    "Modules": [
      {
        // 模块名称(必须与模块 Code 一致)
        "Name": "Admin",
        
        // 数据库名称
        "Database": "Nm_Admin",
        
        // 表前缀
        "Prefix": "",
        
        // 自定义连接字符串(可选,优先级高于全局配置)
        "ConnectionString": ""
      }
    ]
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# 配置项详解

配置项 类型 默认值 说明
Logging bool false SQL 日志开关
Dialect SqlDialect - 数据库类型
Server string - 服务器地址
Port int 0 端口号
UserId string - 用户名
Password string - 密码
CreateDatabase bool true 自动创建数据库
InitData bool true 自动初始化数据
Modules List - 模块配置列表

# 🔧 核心接口详解

# 1. IDbContext(数据库上下文)

文件位置: Data.Abstractions/IDbContext.cs

public interface IDbContext
{
    /// <summary>
    /// 数据库配置
    /// </summary>
    IDbContextOptions Options { get; }

    /// <summary>
    /// 登录信息
    /// </summary>
    ILoginInfo LoginInfo { get; }

    /// <summary>
    /// 数据库是否已存在
    /// </summary>
    bool DatabaseExists { get; }

    /// <summary>
    /// 创建新的连接
    /// </summary>
    IDbConnection NewConnection(IDbTransaction transaction = null);

    /// <summary>
    /// 创建新的工作单元
    /// </summary>
    IUnitOfWork NewUnitOfWork();

    /// <summary>
    /// 获取数据集
    /// </summary>
    IDbSet<TEntity> Set<TEntity>() where TEntity : IEntity, new();

    /// <summary>
    /// 创建数据库和表
    /// </summary>
    void CreateDatabase();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# 2. IDbSet(数据集)

文件位置: Data.Abstractions/IDbSet{T}.cs

# 接口结构

IDbSet (基础接口)
├── Execute 系列 — 执行 SQL 命令
├── ExecuteScalar 系列 — 执行并返回单个值
├── ExecuteReader 系列 — 执行并返回 DataReader
├── QueryFirstOrDefault 系列 — 查询第一条
├── QuerySingleOrDefault 系列 — 查询单条(多条抛异常)
├── QueryMultiple 系列 — 查询多结果集
└── Query 系列 — 查询返回集合

IDbSet<TEntity> (泛型接口,继承 IDbSet)
├── Insert 系列 — 新增
├── BatchInsert 系列 — 批量插入
├── Delete 系列 — 删除
├── SoftDelete 系列 — 软删除
├── Update 系列 — 更新
├── BulkMerge 系列 — 批量合并(Upsert)⭐
├── Get 系列 — 根据主键查询
├── Exists 系列 — 是否存在
├── Find 系列 — 条件查询(返回 IQueryable)
├── 分表操作系列 — 按时间分表
└── Clear 系列 — 清空表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# BulkMerge 系列(批量合并/Upsert)⭐

核心功能:支持"不存在时插入,存在时按条件更新"

// Lambda 版本
IResultModel<BulkMergeResult> BulkMerge(
    IEnumerable<TEntity> entities,
    IUnitOfWork uow = null,
    bool deleteMissing = false,
    Expression<Func<TEntity, bool>> whereExpression = null,
    Expression<Func<TEntity, object>>[] conditionSelectors = null,
    Expression<Func<TEntity, object>>[] includeUpdateSelectors = null,
    params Expression<Func<TEntity, object>>[] ignoreUpdateSelectors);

// 字符串版本
IResultModel<BulkMergeResult> BulkMerge(
    IEnumerable<TEntity> entities,
    IUnitOfWork uow = null,
    bool deleteMissing = false,
    (string SqlCondition, DynamicParameters Parameters) fixedCondition = default,
    string[] matchColumns = null,
    string[] updateColumns = null,
    string[] ignoreColumns = null);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

参数说明:

参数 类型 说明
entities IEnumerable 数据集合
deleteMissing bool 是否删除数据集合中不存在的数据
conditionSelectors Lambda 数组 匹配条件(用于判断是否存在)
includeUpdateSelectors Lambda 数组 更新字段(包含模式)
ignoreUpdateSelectors Lambda 数组 忽略更新的字段(排除模式)

返回值:

属性 说明
InsertedCount 插入的记录数
UpdatedCount 更新的记录数
DeletedCount 删除的记录数

# 💡 使用示例

# 1. 实体定义

using DarkM.Lib.Data.Abstractions.Entities;
using DarkM.Lib.Data.Abstractions.Attributes;

/// <summary>
/// 用户实体
/// </summary>
[Table("User")]
public class UserEntity : EntityBase
{
    /// <summary>
    /// 用户名
    /// </summary>
    [Column("UserName")]
    [Length(50)]
    public string UserName { get; set; }

    /// <summary>
    /// 邮箱
    /// </summary>
    [Column("Email")]
    [Length(100)]
    public string Email { get; set; }

    /// <summary>
    /// 状态
    /// </summary>
    [Column("Status")]
    public Status Status { get; set; } = Status.Enabled;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 2. 仓储接口

public interface IUserRepository : IRepository<UserEntity>
{
    UserEntity GetByUserName(string userName);
    Task<bool> ExistsByUserName(string userName);
}
1
2
3
4
5

# 3. Service 层使用

public class UserService
{
    private readonly IUserRepository _userRepository;
    private readonly ICacheHandler _cache;

    public UserService(IUserRepository userRepository, ICacheHandler cache)
    {
        _userRepository = userRepository;
        _cache = cache;
    }

    public async Task<UserEntity> GetByIdAsync(int id)
    {
        var cacheKey = $"user:{id}";
        var user = await _cache.GetAsync<UserEntity>(cacheKey);
        
        if (user != null)
            return user;

        user = await _userRepository.Get(id);
        
        if (user != null)
            await _cache.SetAsync(cacheKey, user, TimeSpan.FromMinutes(30));

        return user;
    }

    public async Task<bool> CreateAsync(CreateUserRequest request)
    {
        Check.NotNull(request, nameof(request));
        Check.NotNull(request.UserName, nameof(request.UserName));

        if (await _userRepository.ExistsByUserName(request.UserName))
            throw new BusinessException("用户名已存在");

        var user = request.ToEntity();
        user.CreateTime = DateTime.Now;

        return await _userRepository.InsertAsync(user);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 4. 事务操作

using var uow = _dbContext.NewUnitOfWork();
try
{
    await _orderRepository.InsertAsync(order, uow);
    foreach (var item in items)
    {
        item.OrderId = order.Id;
        await _orderItemRepository.InsertAsync(item, uow);
    }
    uow.Commit();
}
catch
{
    uow.Rollback();
    throw;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 5. BulkMerge 使用

// 简单 Upsert(按主键匹配)
var entities = new List<UserEntity>
{
    new UserEntity { Id = 1, Name = "张三" },
    new UserEntity { Id = 2, Name = "李四" }
};

var result = await _dbSet.BulkMergeAsync(entities);

// 按多条件匹配 + 指定更新字段
var result = await _dbSet.BulkMergeAsync(
    entities,
    deleteMissing: false,
    conditionSelectors: new[] { 
        e => e.OrderNo,  // 按订单号匹配
        e => e.ItemId    // 按物品 ID 匹配
    },
    includeUpdateSelectors: new[] { 
        e => e.Quantity,  // 只更新数量
        e => e.Price      // 只更新价格
    }
);

// 同步删除不存在的数据
var result = await _dbSet.BulkMergeAsync(
    entities,
    deleteMissing: true,  // 删除集合中不存在的数据
    whereExpression: e => e.Status == 1  // 附加条件
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 6. 分表操作

// 按天分表
var tableName = _dbSet.GetTableNameByDay(DateTime.Now);
var users = _dbSet.Find(tableName).ToList();

// 数据迁移(保留最近 3 个月)
await _dbSet.MoveDataByMonth(
    keepLength: 3,
    keepType: 2,  // 按月保留
    byColumn: "CreateTime"
);
1
2
3
4
5
6
7
8
9
10

# 🔍 常见问题

# Q1: 如何切换数据库类型?

解决方案:

{
  "Db": {
    "Dialect": 1,  // 0=SqlServer, 1=MySql, 2=SQLite, 3=PostgreSQL
    "Server": "127.0.0.1",
    "Port": 3306,
    "UserId": "root",
    "Password": "root"
  }
}
1
2
3
4
5
6
7
8
9

# Q2: 如何开启 SQL 日志?

解决方案:

{
  "Db": {
    "Logging": true
  }
}
1
2
3
4
5

日志将输出到 Serilog 配置的日志文件中。


# Q3: 如何实现软删除?

解决方案:

  1. 实体继承 EntityBaseWithSoftDelete
public class UserEntity : EntityBaseWithSoftDelete
{
    public string UserName { get; set; }
}
1
2
3
4
  1. 使用软删除方法:
await _userRepository.SoftDeleteAsync(userId);
1
  1. 查询时自动过滤已删除数据:
var user = _userRepository.Get(userId);  // 不会查到已软删除的数据
1

# Q4: 如何执行原生 SQL?

解决方案:

// 通过 IDbContext 执行
using var connection = _dbContext.NewConnection();

// 查询
var users = connection.Query<UserEntity>(
    "SELECT * FROM [User] WHERE Status = @Status", 
    new { Status = 1 });

// 执行命令
var affected = connection.Execute(
    "UPDATE [User] SET Status = 0 WHERE Id = @Id", 
    new { Id = 1 });
1
2
3
4
5
6
7
8
9
10
11
12

# Q5: 如何配置分表策略?

分表命名规则:

方法 格式 示例
GetTableNameByDay {表名}_{yyyyMMdd} Log_20260303
GetTableNameByWeek {表名}_{yyyyww} Log_202609
GetTableNameByMonth {表名}_{yyyyMM} Log_202603
GetTableNameByQuarter {表名}_{yyyyq} Log_20261
GetTableNameByYear {表名}_{yyyy} Log_2026

使用示例:

var tableName = _dbSet.GetTableNameByMonth(DateTime.Now);
await _dbSet.InsertAsync(entity, tableName: tableName);
1
2

# 📚 相关文档


# 🔗 参考链接


最后更新: 2024-09-20