You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为多阶段列切换场景适配Dapper数据模型?

问题:Dapper模型适配多阶段数据表的标记化切换

我现在碰到这么个棘手的事儿:手里有多张数据表,随着时间迭代,它们的data、token_data这类列一直在切换,总共分4个阶段:

  • 阶段0:只有data列(存的是明文数据)
  • 阶段1:同时有data和token_data列
  • 阶段2:换成了token_data和clear_data列
  • 阶段3:又只剩data列,但这时候的data已经是标记化后的数据了

现在所有的Dapper和数据库模型都是基于阶段0设计的,我想让同一个模型能适配这四个阶段,之前找过OptionalColumn特性但没找到合适的用法。之前试过这么个方案,但感觉不太好:

// 方案不佳
[Column("Name")] 
public string Name { 
    get { return AppSettings.TokenizationEnabled ? this.TokenName : _name; } 
    set { _name = value; } 
}

可行的适配方案

方案1:自定义类型映射+全局配置开关

Dapper允许我们自定义类型映射,结合全局的标记化阶段配置,可以动态控制属性和数据库列的绑定关系,不用在模型里写一堆条件判断:

  1. 先在模型里把所有可能用到的属性都加上,比如Data、TokenData、ClearData,暂时不用加[Column]特性
  2. 在程序启动时,根据当前阶段配置全局的类型映射:
// 先拿到当前的标记化阶段
var currentStage = AppSettings.TokenizationStage;

// 给你的模型设置自定义属性映射
SqlMapper.SetTypeMap(typeof(YourDataModel), new CustomPropertyTypeMap(
    typeof(YourDataModel),
    (modelType, columnName) =>
    {
        switch (currentStage)
        {
            case 0:
                // 阶段0只映射data列到Data属性
                return columnName.Equals("data", StringComparison.OrdinalIgnoreCase) 
                    ? modelType.GetProperty(nameof(YourDataModel.Data)) 
                    : null;
            case 1:
                // 阶段1同时映射data和token_data到对应属性
                if (columnName.Equals("data", StringComparison.OrdinalIgnoreCase))
                    return modelType.GetProperty(nameof(YourDataModel.Data));
                if (columnName.Equals("token_data", StringComparison.OrdinalIgnoreCase))
                    return modelType.GetProperty(nameof(YourDataModel.TokenData));
                return null;
            case 2:
                // 阶段2把clear_data映射到Data属性,token_data映射到TokenData
                if (columnName.Equals("clear_data", StringComparison.OrdinalIgnoreCase))
                    return modelType.GetProperty(nameof(YourDataModel.Data));
                if (columnName.Equals("token_data", StringComparison.OrdinalIgnoreCase))
                    return modelType.GetProperty(nameof(YourDataModel.TokenData));
                return null;
            case 3:
                // 阶段3的data列是标记化数据,映射到TokenData属性
                return columnName.Equals("data", StringComparison.OrdinalIgnoreCase) 
                    ? modelType.GetProperty(nameof(YourDataModel.TokenData)) 
                    : null;
            default:
                return null;
        }
    }
));

这种方式的好处是所有映射逻辑集中在一处,切换阶段只需要改配置值,模型本身不用频繁修改。

方案2:用Dapper拦截器动态修改SQL

如果不想动模型,还可以用Dapper的拦截器功能,在SQL执行前动态替换列名,适配不同阶段:

  1. 模型还是用阶段0的结构(比如只保留Data属性)
  2. 实现一个拦截器,根据当前阶段替换SQL里的列名:
public class TokenizationStageInterceptor : SqlMapper.IDbCommandInterceptor
{
    public void ReaderExecuting(IDbCommand command, SqlMapper.CommandDefinition commandDefinition)
    {
        var currentStage = AppSettings.TokenizationStage;
        switch (currentStage)
        {
            case 2:
                // 把SQL里的[data]替换成[clear_data],适配阶段2的列名
                command.CommandText = command.CommandText.Replace("[data]", "[clear_data]");
                break;
            case 3:
                // 阶段3的data是标记化数据,直接替换成token_data的逻辑(如果需要)
                command.CommandText = command.CommandText.Replace("[data]", "[token_data]");
                break;
            case 1:
                // 如果需要同时读取明文和标记化数据,可以在这里修改SQL,比如加上token_data列
                // command.CommandText = command.CommandText.Replace("SELECT *", "SELECT *, token_data FROM");
                break;
        }
    }

    // 其他接口方法可以留空实现
    public void NonQueryExecuting(IDbCommand command, SqlMapper.CommandDefinition commandDefinition) { }
    public void ScalarExecuting(IDbCommand command, SqlMapper.CommandDefinition commandDefinition) { }
    public void ReaderExecuted(IDbCommand command, SqlMapper.CommandDefinition commandDefinition, SqlMapper.IReaderReader reader) { }
    public void NonQueryExecuted(IDbCommand command, SqlMapper.CommandDefinition commandDefinition, int result) { }
    public void ScalarExecuted(IDbCommand command, SqlMapper.CommandDefinition commandDefinition, object result) { }
}

然后在程序启动时注册这个拦截器:

SqlMapper.AddInterceptor(new TokenizationStageInterceptor());

这个方案对业务代码几乎没有侵入,所有适配逻辑都在拦截器里,非常适合快速切换阶段的场景。

方案3:抽象基类+阶段子类

如果不同阶段有特殊的业务逻辑(比如写入时需要同时更新明文和标记化列),可以用抽象基类+子类的方式隔离逻辑:

  1. 先定义一个抽象基类,包含业务层需要的核心属性:
public abstract class BaseDataModel
{
    // 业务层只关心这个属性
    public abstract string Data { get; set; }
}
  1. 针对每个阶段实现具体的子类,处理列映射和阶段专属逻辑:
[Table("YourDataTable")]
public class Stage0DataModel : BaseDataModel
{
    [Column("data")]
    public override string Data { get; set; }
}

[Table("YourDataTable")]
public class Stage1DataModel : BaseDataModel
{
    [Column("data")]
    public string ClearData { get; set; }

    [Column("token_data")]
    public string TokenizedData { get; set; }

    // 根据配置决定读取/返回哪个值
    public override string Data
    {
        get => AppSettings.UseTokenizedData ? TokenizedData : ClearData;
        set
        {
            ClearData = value;
            // 这里可以加入标记化逻辑,自动生成TokenizedData
            TokenizedData = TokenizationService.Tokenize(value);
        }
    }
}

// 阶段2和阶段3的子类同理,对应各自的列映射

然后在业务代码里根据配置动态选择使用哪个子类:

var modelType = AppSettings.TokenizationStage switch
{
    0 => typeof(Stage0DataModel),
    1 => typeof(Stage1DataModel),
    2 => typeof(Stage2DataModel),
    3 => typeof(Stage3DataModel),
    _ => typeof(Stage0DataModel)
};

// 查询时传入对应的类型,再转成基类使用
var dataList = connection.Query(modelType, "SELECT * FROM YourDataTable")
    .Cast<BaseDataModel>()
    .ToList();

这种方式逻辑隔离清晰,适合阶段间业务差异较大的场景。


补充:关于OptionalColumn的替代方案

Dapper本身没有OptionalColumn特性,但可以通过全局配置忽略未映射的列,避免因为列不存在报错:

// 全局设置,让Dapper忽略模型里没有对应列的属性
SqlMapper.Settings.IgnoreUnmappedProperties = true;

或者在单个查询里临时启用:

var data = connection.Query<YourDataModel>(
    "SELECT * FROM YourDataTable",
    flags: CommandFlags.IgnoreUnmapped
);

这个设置能帮你避免在切换阶段时,因为某些列不存在而抛出异常。

内容的提问来源于stack exchange,提问作者steakoverflow

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 09:18:00