惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Security Archives - TechRepublic
Security Archives - TechRepublic
N
News and Events Feed by Topic
Last Week in AI
Last Week in AI
博客园 - 司徒正美
The GitHub Blog
The GitHub Blog
O
OpenAI News
The Last Watchdog
The Last Watchdog
T
The Blog of Author Tim Ferriss
M
MIT News - Artificial intelligence
P
Proofpoint News Feed
Forbes - Security
Forbes - Security
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
有赞技术团队
有赞技术团队
Jina AI
Jina AI
GbyAI
GbyAI
V
Vulnerabilities – Threatpost
L
LangChain Blog
Vercel News
Vercel News
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
AI
AI
博客园 - 聂微东
W
WeLiveSecurity
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Scott Helme
Scott Helme
罗磊的独立博客
Martin Fowler
Martin Fowler
S
Security Affairs
T
Tor Project blog
Recent Announcements
Recent Announcements
F
Fortinet All Blogs
美团技术团队
C
Cisco Blogs
PCI Perspectives
PCI Perspectives
Recent Commits to openclaw:main
Recent Commits to openclaw:main
S
Security @ Cisco Blogs
T
Threat Research - Cisco Blogs
A
About on SuperTechFans
Cisco Talos Blog
Cisco Talos Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
I
Intezer
B
Blog
WordPress大学
WordPress大学
I
InfoQ
G
Google Developers Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
V
V2EX
P
Privacy & Cybersecurity Law Blog
雷峰网
雷峰网

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角 .NET Core 原生 DI 扩展之基于名称的注入实现 - 元视角
Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角
飞鸿踏雪 · 2020-09-05 · via 元视角

话说最近这两周里,被迫官宣996的生活实在是无趣,在两周时间里安排三周的工作量,倘若用丞相的口吻来说,那便是: 我从未见过有如此厚颜无耻之人。无法为工作的紧急程度排出优先级,这便是身为肉食者们的。古人云:肉食者鄙,未能远谋,诚不欺我也。一味地追求快速迭代,“屎”山越滚越高没有人在乎;一味地追求功能叠加,技术债务越来越多没有人在乎。所以,本着“多一事不如少一事”的原则,直接通过 Dapper 写 SQL 语句一样没有问题,因为被压榨完以后的时间只能写这个。在今天的这篇博客里,我想和大家分享的是,Dapper.Contrib在操作 Oracle 数据库时引发 ORA-00928: 缺失 SELECT 关键字 这一错误背后的根本原因,以及 Dapper 作为一个轻量级 ORM 在设计上做出的取舍。

问题回顾

在使用 Dapper.Contrib 操作 Oracle 数据库的时候,通过 Insert() 方法来插入一个实体对象,此时,会引发 ORA-00928: 缺失 SELECT 关键字 这种典型的 Oracle 数据库错误,对于经常使用 Dapper 的博主而言,对于 @ 还是 : 这种无聊的语法还是有一点经验的,在尝试手写 SQL 语句后,发现使用 Dapper 提供的 Execute() 扩展方法一点问题都没有,初步判定应该是 Dapper.Contrib 这个扩展库的问题,在翻阅 Dapper 的源代码以后,终于找到了问题的根源所在,所以,下面请跟随博主的目光,来一起解读解读 Dapper.Contrib 这个扩展库,相信你看完以后就会明白,为什么这里会被 Oracle 数据库摆上一道,以及为什么它至今都不考虑合并 Oracle 数据库相关的 PR。

原因分析

众所周知,Dapper 的核心其实就是一个 SqlMapper ,它提供的 Query() 和 Execute() 接口本身都是附加在 IDbConnection 接口上的扩展方法,所以,最基础的 Dapper 用法其实是伴随着 SQL 语句和以匿名对象为主的参数化查询,这可以说是 Dapper 的核心,而 Dapper.Contrib 在这个基础上提供了 Get()、Insert()、Delete() 和 Update() 等等常见的 CRUD 方法,这些方法都针对的是单主键的表,让 Dapper有了一点 ORM 的感觉,可惜的是 Dapper.Contrib 的实现是不完整的,主要是指下面两个方面,即:第一,官方未能提供 Oracle 版本的 ISqlAdapter。第二,兼容不同数据库自增 ID 的实现,让官方在处理 Id 的参数化查询时束手束脚,对 ISqlAdapter 的设计并不全面。

Oracle 版本的 ISqlAdapter

首先,第一个结论,Dapper.Contrib 没有实现 Oracle 版本的 ISqlAdapter 。关于这个接口,我们可以在 SqlMapperExtensions 这个类中找到定义,而 Dapper.Contrib 内部实际上是维护了一个字典 AdapterDictionary ,在 SqlMapperExtensions.cs 文件的第 62 行 ~ 第 73 行,我们可以注意到,其内部提供了 6 种 ISqlAdapter 的实现,且默认为 SqlServerAdapter

private static readonly ISqlAdapter DefaultAdapter = new SqlServerAdapter();
private static readonly Dictionary<string, ISqlAdapter> AdapterDictionary
    = new Dictionary<string, ISqlAdapter>(6)
      {
          ["sqlconnection"] = new SqlServerAdapter(),
          ["sqlceconnection"] = new SqlCeServerAdapter(),
          ["npgsqlconnection"] = new PostgresAdapter(),
          ["sqliteconnection"] = new SQLiteAdapter(),
          ["mysqlconnection"] = new MySqlAdapter(),
          ["fbconnection"] = new FbAdapter()
    };

一个自然而然的问题是,这个 ISqlAdapter 接口是做什么的呢?为什么说 Dapper.Contrib 没有实现 Oracle 版本的 ISqlAdapter 呢?如果我们看一下 ISqlAdapter 的定义,就可以了解到其作用是告诉 Dapper ,应该怎么样处理数据库里的自增 ID、怎么样表示 Column = Value 这样的结构,以及怎么样处理列名:

public partial interface ISqlAdapter
{
    int Insert(IDbConnection connection, IDbTransaction transaction, 
        int? commandTimeout, string tableName, string columnList, 
        string parameterList, IEnumerable<PropertyInfo> keyProperties, 
        object entityToInsert
    );
    void AppendColumnName(StringBuilder sb, string columnName);
    void AppendColumnNameEqualsValue(StringBuilder sb, string columnName);
}

这里以 MySqlAdapter 的实现为例:

public partial class MySqlAdapter : ISqlAdapter
{
    public int Insert(IDbConnection connection, IDbTransaction transaction, 
        int? commandTimeout, string tableName, string columnList, 
        string parameterList, IEnumerable<PropertyInfo> keyProperties, 
        object entityToInsert
    )
    {
        var cmd = $"insert into {tableName} ({columnList}) values ({parameterList})";
        connection.Execute(cmd, entityToInsert, transaction, commandTimeout);
        var r = connection.Query("Select LAST_INSERT_ID() id", transaction: transaction, commandTimeout: commandTimeout);

        var id = r.First().id;
        if (id == null) return 0;
        var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray();
        if (propertyInfos.Length == 0) return Convert.ToInt32(id);

        var idp = propertyInfos[0];
        idp.SetValue(entityToInsert, Convert.ChangeType(id, idp.PropertyType), null);

        return Convert.ToInt32(id);
    }

    public void AppendColumnName(StringBuilder sb, string columnName)
    {
        sb.AppendFormat("`{0}`", columnName);
    

    public void AppendColumnNameEqualsValue(StringBuilder sb, string columnName)
    {
        sb.AppendFormat("`{0}` = @{1}", columnName, columnName);
    }
}

相信看到这里的时候,大家会和我一样感到失望,因为 Dapper 的底层依然是在拼 SQL ,尤其是看到 AppendColumnNameEqualsValue() 这个方法的时候,会有一种恍然大明白的感觉,因为 @ 这个符号对于 Dapper 的参数化查询而言,实在是熟悉得不能再熟悉了。我们都知道为 Dapper 写 SQL 语句的时候,要对 Oracle 区别对待,因为这个奇葩非要用 : 这个奇怪的符号。回到我们一开始的问题,为啥 Dapper.Contrib 在 Oracle 环境下会提示 ORA-XXXXX 这种鬼都看不明白的错误,因为它在处理 SQL 的语句的时候依然使用的是 @ 这个符号。这又是为什么呢?因为当指定的 IDbConnectionAdapterDictionary 中不存在的时候,它会使用默认的 SqlServerAdapter ,显然,全世界只有 Oracle 这个奇葩会用 : 这个奇怪的符号。我们不是在调用 Insert() 方法的时候提示这个错误吗?那么 Dapper.Contrib 是怎么实现 Insert() 方法的呢?这个部分实现主要在第 352 行 ~ 第 360 行:

var adapter = GetFormatter(connection);

for (var i = 0; i < allPropertiesExceptKeyAndComputed.Count; i++)
 {
    var property = allPropertiesExceptKeyAndComputed[i];
    adapter.AppendColumnName(sbColumnList, property.Name);  //fix for issue #336
    if (i < allPropertiesExceptKeyAndComputed.Count - 1)
       sbColumnList.Append(", ");
 }

显然,这部分是按照属性名去组织 columnListparameterList 的过程,对于 Oracle ,永远是充满吐槽的,比如不加双引号则强制大写的设定,这意味着如果你的表名或者字段名是区分大小写的话,在 Oracle 这里都要加上双引号,这对 Dapper.Contrib 有什么影响呢?原本我们只需要给实体添加[Table]标签即可,而现在你不得不考虑带上反斜杠转义,甚至当你需要为 DBeaver 下载一个 JDBC 的驱动的时候,甲骨文这家公司居然要强制你去注册,对于一个习惯像·.NET CoreGCCPythonLuaNode 这样开箱即用的人来说,这就像强迫你注册一大堆真实信息,然后发现 API 接口完全无法匹配你的需求一样痛苦。关于 GetFormatter() 方法,它和我们猜想的完全一致:

private static ISqlAdapter GetFormatter(IDbConnection connection)
{
   var name = GetDatabaseType?.Invoke(connection).ToLower() ?? connection.GetType().Name.ToLower();
   return AdapterDictionary.TryGetValue(name, out var adapter) ? adapter : DefaultAdapter;
}

好了,在明白了以上种种因果关系以后,我们现在来考虑如何解决 Oracle 的问题。按照人类最直观的思维,既然它没有实现 Oracle 版本的 ISqlAdapter ,我自己实现一个不就好啦:

public class OracleSqlAdapter : ISqlAdapter
{
    public void AppendColumnName(StringBuilder sb, string columnName)
   {
        sb.AppendFormat("{0}", columnName);
   }

   public void AppendColumnNameEqualsValue(StringBuilder sb, string columnName)
   {
        sb.AppendFormat("{0} = :{1}", columnName, columnName);
   }

   public int Insert(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, 
            string columnList, string parameterList, IEnumerable<PropertyInfo> keyProperties, object entityToInsert)
   {
        var sql = $"insert into {tableName} ({columnList}) values ({parameterList})";
        return connection.Execute(sql, entityToInsert, transaction, commandTimeout);
    }

    public Task<int> InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, 
            string columnList, string parameterList, IEnumerable<PropertyInfo> keyProperties, object entityToInsert)
    {
         var sql = $"insert into {tableName} ({columnList}) values ({parameterList})";
         return connection.ExecuteAsync(sql, entityToInsert, transaction, commandTimeout);
    }
}

坦白说, Dapper.Contrib 这种纯静态类的设计,完全就不给别人留扩展的口子,为此,扩展方法 + 反射搞一个突破口:

 public static class SqlAdapterrExtensions
 {
    public static void UseSqlAdapter<TSqlAdapter>(this IDbConnection connection, TSqlAdapter sqlAdapter)
        where TSqlAdapter : ISqlAdapter, new()
    {
        var adapters = (Dictionary<string, ISqlAdapter>)
            typeof(SqlMapperExtensions)
            .GetField("AdapterDictionary", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)
            ?.GetValue(null);

        var connectionType = connection.GetType().Name.ToLower();
        if (adapters != null && !adapters.ContainsKey(connectionType))
            adapters?.Add(connectionType, sqlAdapter);
    }
}

这样,我们不但可以满足眼下,还可以着眼未来,虽然未来有时候挺遥远,但梦想还是要有的,开闭原则,我做到了!改进后,我们这样处理即可:

connection = new OracleConnection(ConnectionStrings.Default);
connection.UseSqlAdapter(new OracleSqlAdapter());

此时,我们发现,我们解决了 Insert() 的问题,但随之而来的,Get()、Delete()、Update() 这一系列和主键相关的方法,都因为 Dapper.Contrib 中的主键设计而出现了问题,而这就是我们接下来要讲的主键 Id 参数化问题。

主键 Id 参数化问题

当我谈起这个问题的时候,我对于 Dapper.Contrib 中支持自增 ID 的坚持是怀疑的,因为在分布式盛行的今天,有大量的分布式 ID 生成方案供我们选择,比如基于 Redis 的号段策略,基于雪花算法的 ID 生成等等。大家会注意到我实现的 OracleSqlAdapter 在实现 Insert() 方法的时候简化了大量代码,这是因为我真的不知道,怎么从 Oracle 中获取一个新生成的 ID,尤其是这个 ID 居然还要依赖一个我听都没有听说过的“序列”,而之所以要在 ISqlAdapter 中实现 Insert() 方法,最根本的原因就是,各个数据库对于自增 ID 的实现是不一样的,比如 MySQL 中使用的是 SCOPE_IDENTITY(),而 MSSQL 中使用的则是 SCOPE_IDENTITY() ,就因为这一点点差异,我们就必须要去折腾一遍,可以说, Dapper.Contrib 不支持 Oracle 的一个重要原因,就是在 Oracle 下实现自增 ID 太麻烦了。

既然大家都不用自增 ID 了,为什么还要在一个通用的 ORM 里折腾这个呢?说实话,我真担心有一天自增 ID 会溢出,谁让每个数据库里的上限都不一样呢?另一方面,既然 Id 在每个数据库的实现都不一样,那么,作为 Id 本身应该考虑放到 ISqlAdapter 接口中由使用者来实现啊,可偏偏 ISqlAdapter 里只定义了一个 Insert() 方法,所以,就算我们实现了 OracleSqlAdapter ,一样无法解决 Insert() 方法以外的其它方法在 Oracle 下面的问题,正因为如此,默认的 @ 符号在 Oracle 环境下下没有被完全替换掉,这就需要修改 Dapper.Contrib 的底层代码,这真的是一个不好的设计,因为使用者完全没有办法通过重写来覆盖某些默认行为,我们一起来看看,需要修改哪些地方:

public static T Get<T>(this IDbConnection connection, dynamic id, 
IDbTransaction transaction = null, int? commandTimeout = null) where T : class
{
    var type = typeof(T);

    if (!GetQueries.TryGetValue(type.TypeHandle, out string sql))
    {
        var key = GetSingleKey<T>(nameof(Get));
        var name = GetTableName(type);

        //第一个坏事儿的地方,为什么不用AppendColumnName()方法?
        sql = $"select * from {name} where {key.Name} = @id";
        GetQueries[type.TypeHandle] = sql;
    }

    var dynParams = new DynamicParameters();
    //第二个坏事的地方,什么不用AppendColumnName()方法??
    dynParams.Add("@id", id);

    //以下代码已省略
}
public static long Insert<T>(this IDbConnection connection, T entityToInsert,
IDbTransaction transaction = null, int? commandTimeout = null) where T : class
{
    //以上代码已省略

    var sbParameterList = new StringBuilder(null);
    for (var i = 0; i < allPropertiesExceptKeyAndComputed.Count; i++)
    {
        //第三个坏事的地方,什么不用AppendColumnName()方法???
        var property = allPropertiesExceptKeyAndComputed[i];
        sbParameterList.AppendFormat("@{0}", property.Name);
        if (i < allPropertiesExceptKeyAndComputed.Count - 1)
            sbParameterList.Append(", ");
    }

    //以下代码已省略
}

其实,仔细阅读 Update() 和 Delete() 两个方法的实现,就会发现它们都非常完美地避开了这一点,就是不知道为什么只有两个方法采用了不同地方式去拼接 SQL ,当然,这里我们会意识到有个列名的问题,尤其是在需要区分大小写的情况下,为此,我们可能需要去定义一个 ColumnAttribute,还能说什么呢?请和我大声地吐槽:**垃圾 Oracle !**你看,就为了这一点点差异,我们不得不去额外写一点代码,所以,喊了很多年的去 IOE,我表示举双手赞成。

事实上,社区里已经有类似的PR,可因为改动的范围比较大,官方至今都没有考虑过将其合并到主干分支上,所以,这个问题一直没有解决,这是一个悲伤的故事。

相关思考

在阅读 Dapper 源码的同时,我查阅了一个和 Dapper.Contrib 类似的项目:DapperExtension,我发现这个项目目前处在“荒废”的状态,因为它遇到了相同的问题,即 SQL 这门看起来统一实则相当不统一的语言,因为每一个数据库厂商几乎都在给标准“添砖加瓦“,就以自增 ID 为例,MySQL、MSSQL、Oracle 居然是三种不同的实现方式,尤其是 Oracle 这个奇葩,居然还需要定义一个序列来解决这个问题,这个奇葩给数据库加注释都那么另类,这带来的问题是什么?Dapper.Contrib 无力去实现 Oracle 的自增 ID 而放弃了 Oracle ,所以,即使社区里提交了 PR,因为实现方式有点脏,官方一直没有合并到主干上去。

再回过头来看 Dapper.Contrib 支持自增 ID 的举动,总会觉得有点不合时宜,因为不同数据库自增 ID 的上限不一样不说,现在都普遍在分布式的环境中,数据库的自增 ID 其实是非常鸡肋的功能,而实际应用中常常会用 Redis 、雪花算法等来实现分布式 ID,所以,当你回顾历史发展的趋势的时候,就会感慨有标准化的东西该多好,并不是说这个世界不需要多样性,显然这是一个标准约束性不强的领域,看起来大家都实现了 SQL,无一例外地都夹藏了私货,对于商业行为而言,这无可厚非;可对于这个世界而言,这无疑增加了工作量。

有时候,当一个行业没有什么标准的时候,到底是突破勇气去率先制定标准,还是放弃自我去迎合各种不成文的规则,对于企业而言,是战略上的一种选择;而对于个人而言,其实是人生的一种选择。当彼时青春年少的人们,竞相以标新立异为荣的时候,如果想到有一天,终究要活成千篇一律的人生,为了生活而选择跪着的时候,内心又会有什么不一样的举动呢?

本文小结

本文分析了 Dapper.Contrib 这个扩展库,在搭配 Oracle 数据库使用时遇到 ORA-XXXXX 系列错误的原因及相应地处理方法,这个问题的表象是 Dapper.Contrib 没有实现 OracleSqlAdapter ,而更深层的原因,实际上是 Dapper.Contrib 选择支持自增 ID 而带来的 SQL 标准差异化问题。因为不同的数据库在实现自增 ID 时的机制不同,Oracle 甚至需要引入序列这个概念,这种差异化,增加了 Dapper 各个扩展库维护的工作量,这是官方一直不愿意实现 OracleSqlAdapter 的原因,其次, Dapper.Contrib 底层设计不合理,除了 Insert() 方法以外,其它依赖主键的方法都没有提供扩展接口,导致使用者只能通过修改底层代码的方式解决问题,这严重违反开闭原则。好了,这是一篇利用 996 闲暇(可能是指做梦)写的一篇博客,如果文章中有什么不周到的地方,欢迎大家在博客下面给我留言,谢谢,晚安!