如何为多阶段列切换场景适配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允许我们自定义类型映射,结合全局的标记化阶段配置,可以动态控制属性和数据库列的绑定关系,不用在模型里写一堆条件判断:
- 先在模型里把所有可能用到的属性都加上,比如
Data、TokenData、ClearData,暂时不用加[Column]特性 - 在程序启动时,根据当前阶段配置全局的类型映射:
// 先拿到当前的标记化阶段 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执行前动态替换列名,适配不同阶段:
- 模型还是用阶段0的结构(比如只保留
Data属性) - 实现一个拦截器,根据当前阶段替换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:抽象基类+阶段子类
如果不同阶段有特殊的业务逻辑(比如写入时需要同时更新明文和标记化列),可以用抽象基类+子类的方式隔离逻辑:
- 先定义一个抽象基类,包含业务层需要的核心属性:
public abstract class BaseDataModel { // 业务层只关心这个属性 public abstract string Data { get; set; } }
- 针对每个阶段实现具体的子类,处理列映射和阶段专属逻辑:
[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
相关产品推荐
相关产品推荐

