☰
C# Oracle实体类自动生成:数据字典、类型映射与避坑
2026/11/2 12:58:05 网站建设 项目流程

简介:面向在 .NET 平台对接 Oracle 数据库的开发者,这份 C# 与 Oracle 实体类生成工具及配套示例,聚焦于用 Entity Framework 将表结构映射为 C# 模型、减少手写数据访问层代码的工作。压缩包共 62 个文件,约 2.99MB,以 cs 源码、dll 依赖、xml 与 xaml 配置及界面定义、ico 图标、pdb 调试符号、exe 程序与 sln 解决方案为主体,覆盖连接配置、实体生成、界面交互和数据库辅助等模块,便于解压后查看与运行。内容围绕连接字符串配置、表与视图选择、实体类属性映射、主键及表名注解、DbContext 使用等环节展开,并涉及 Code First 迁移、Repository 与 Unit of Work、查询优化等扩展思路,可帮助读者理解从 Oracle 元数据到 C# 实体的完整链路。已有 214 人学习,适合需要快速搭建数据访问层、对照生成结果排查映射问题的初中级开发者参考。

1. 八十张表、三天工期:c# oracle 实体类到底该不该手写

上一轮改造收尾,临时接到一个老系统的只读查询需求,库里八十多张表,字段名全是T_ORD_MST、BILL_NO这种风格。同事的第一反应是打开编辑器一张表一张表敲实体类,敲到第三张就开始骂人——不是人懒,是这活手写没有任何收益:字段名抄错一个字母,编译照样通过,跑起来才报“列不存在”,改一次烧一次联调时间。c# oracle 这条链路上,实体类本来就是数据库结构的镜像,镜像就不该靠人眼对齐。真正省时间的做法是从数据字典把表结构读出来,用代码生成代码,中间只留一层可配置的类型映射和命名规则。下面这条流水线覆盖元数据查询、类型映射、模板拼装、脚手架对比和翻车点,适合手里有存量 Oracle 库、要在 .NET 项目里补一套实体的同学。

2. 先把数据字典摸清:三张视图和一张类型映射表

生成器写得漂不漂亮不重要,元数据取错了后面全白搭。Oracle 里表结构、列信息、注释、主键分布在不同的视图里,只查ALL_TAB_COLUMNS能拿到列名和类型,但拿不到主键、拿不到中文注释,生成的实体就是“能用但没法维护”的半成品。这一章把要查的视图和字段含义先钉死,后面写代码只是把结果搬过去。

2.1 ALL_TAB_COLUMNS 里哪些字段真正有用

ALL_TAB_COLUMNS是列级信息的入口,USER_TAB_COLUMNS只看当前用户下的对象,DBA_TAB_COLUMNS需要 DBA 权限。做生成器一般用ALL_前缀,配一个 schema 参数,这样换库不用改代码。字段不少,真正要读的就那么几个:

字段含义生成时怎么用
TABLE_NAME表名决定类名
COLUMN_ID列序号排序用,保证字段顺序和表一致
COLUMN_NAME列名决定属性名
DATA_TYPE数据类型映射的第一判据
DATA_LENGTH字节长度主要参考,CHAR语义要用CHAR_LENGTH
DATA_PRECISION数字精度NUMBER映射成int还是decimal靠它
DATA_SCALE小数位大于 0 就必须用decimal
NULLABLEY/N决定值类型要不要加?
DATA_DEFAULT默认值表达式可选,用于生成注释或初始化

几个容易忽略的点。DATA_PRECISION和DATA_SCALE在非NUMBER类型上是NULL,读的时候必须用IsDBNull判断,直接强转Convert.ToInt32会在VARCHAR2列上抛异常。DATA_DEFAULT在 11g 之后是LONG类型,ODP.NET 用GetString能读出来,但读出来是原始表达式,比如SYSDATE或者'0',带引号,不要直接往代码里贴。CHAR_USED字段区分BYTE和CHAR,如果库里有VARCHAR2(20 CHAR)这种建表语句,DATA_LENGTH是字节数,CHAR_LENGTH才是字符数,生成校验代码时要按CHAR_LENGTH走。

注释在另外两张表:ALL_TAB_COMMENTS的COMMENTS是表注释,ALL_COL_COMMENTS的COMMENTS是列注释。很多国内项目的中文业务含义全写在这里,T_ORD_MST.AMT1这种字段不带上注释,生成的实体等于废纸。查询时用LEFT JOIN关联,别用INNER JOIN,没有注释的表会被整张过滤掉——这个坑我踩过一次,生成出来的实体少了十几张表,排查了半天才发现是 join 写错了。

主键查ALL_CONSTRAINTS和ALL_CONS_COLUMNS两张表联查:

SELECT c.OWNER, c.TABLE_NAME, cc.COLUMN_NAME, cc.POSITION FROM ALL_CONSTRAINTS c JOIN ALL_CONS_COLUMNS cc ON c.OWNER = cc.OWNER AND c.CONSTRAINT_NAME = cc.CONSTRAINT_NAME WHERE c.CONSTRAINT_TYPE = 'P' AND c.OWNER = :owner ORDER BY c.TABLE_NAME, cc.POSITION

POSITION就是复合主键里的顺序,ORDER BY不能省,否则复合主键的字段顺序随机,生成的HasKey配置每次都不一样,diff 会很难看。CONSTRAINT_TYPE = 'P'只查主键,唯一约束是'U',如果业务上把唯一索引当主键用,得单独补一查。

2.2 NUMBER 到 C# 类型的映射规则

类型映射是生成器唯一需要“判断”的地方,其余全是搬运。Oracle 的类型系统比 C# 宽,最麻烦的是NUMBER:同一个类型名,可能是布尔、可能是金额、可能是雪花 ID。

常见做法是拿DATA_PRECISION和DATA_SCALE两个值做判断:

Oracle 声明C# 类型依据
NUMBER(1)~NUMBER(4)int精度不超过 4 位
NUMBER(5)~NUMBER(9)int同上,9 位内 int 够装
NUMBER(10)~NUMBER(18)long长整型安全区
NUMBER(19+)或DATA_SCALE > 0decimal金额、ID 都按 decimal 处理最稳
NUMBER(无精度)decimal无约束就用最宽的
BINARY_FLOATfloat对应单精度
BINARY_DOUBLEdouble对应双精度

字符串和日期这几类没什么争议:VARCHAR2、NVARCHAR2、CHAR、NCHAR、CLOB、NCLOB、LONG一律string;DATE映射DateTime,注意 Oracle 的DATE带时分秒,别当纯日期用;TIMESTAMP也是DateTime,TIMESTAMP WITH TIME ZONE用DateTimeOffset;BLOB和RAW用byte[];ROWID用string;XMLTYPE用string最省事。

NUMBER(1)最容易翻车。有人直接映射成bool,结果库里存的是0/1/2,读出来就炸;还有人映射成char(1)存'Y'/'N',那更麻烦。我一般的处理是NUMBER(1)一律映射int,业务层自己包一层枚举,生成器不替业务做决定。真要用bool,就在模板里加一个字段级的覆盖规则,用列名白名单控制,别全局开。

DATA_SCALE大于 0 就往decimal走这条也要留个口子:有些老系统用NUMBER(10,2)存时间戳或者编码,映射成decimal虽然不会丢精度,但用起来很别扭。这种只能靠配置覆盖,生成器留一个TypeOverrides字典,键是表名.列名,值是目标 C# 类型,跑一次看一遍再补,比一开始就想做全自动靠谱。

3. 用 ODP.NET 写一个实体生成器:连接、查询、拼代码

现成的代码生成工具不少,但老库的表名、注释、类型覆盖这些东西,通用工具很难一次配好,改配置的时间往往比写一个专用生成器还长。自己写一个两百行的控制台程序,逻辑全在手上,后面加规则就是改几行的事。

3.1 驱动选型和连接串

.NET Framework 项目用Oracle.ManagedDataAccess,.NET 6/8 用Oracle.ManagedDataAccess.Core,两个包名不同,别装错。System.Data.OracleClient已经废弃多年,不要再碰。文件版本不需要装 Oracle 客户端,托管驱动是纯托管实现,这点对 CI 环境很友好。

连接串两种写法:

User Id=app_ro;Password=***;Data Source=10.0.0.12:1521/orclpdb;Pooling=true;Connection Timeout=15;
User Id=app_ro;Password=***;Data Source=ORCL_PROD;TNS_ADMIN=D:\oracle\network\admin;

第一种是 EZConnect,主机:端口/服务名,适合直连;第二种走 TNS 别名,需要tnsnames.ora,适合已经有运维配好的环境。生成器用的账号只要有SELECT数据字典的权限就够,别图省事用业务账号甚至 DBA 账号,元数据查询权限给到只读账号,出问题影响面小。

Pooling=true在生成器里其实用不上,因为只跑一次,但带上没坏处。真正要注意的是Connection Timeout,默认 15 秒,网络不通的时候程序会卡住,显式写出来方便排查。

3.2 查询元数据并落成模型

先定义承载列信息的模型,字段和ALL_TAB_COLUMNS对应,少一个字段后面就要多查一次库。

public class ColumnMeta { public string TableName { get; set; } public string ColumnName { get; set; } public int ColumnId { get; set; } public string DataType { get; set; } public int? DataPrecision { get; set; } public int? DataScale { get; set; } public int? CharLength { get; set; } public bool IsNullable { get; set; } public string Comment { get; set; } public string DefaultValue { get; set; } } public class TableMeta { public string TableName { get; set; } public string TableComment { get; set; } public List<ColumnMeta> Columns { get; set; } = new(); public List<string> PrimaryKeys { get; set; } = new(); }

查询部分用一条 SQL 把表和注释拼起来,避免 N+1。注意别名,ALL_TAB_COLUMNS的COMMENTS在ALL_COL_COMMENTS里,表注释用子查询取一次就行:

var sql = @" SELECT c.TABLE_NAME, c.COLUMN_ID, c.COLUMN_NAME, c.DATA_TYPE, c.DATA_PRECISION, c.DATA_SCALE, c.CHAR_LENGTH, c.NULLABLE, cm.COMMENTS FROM ALL_TAB_COLUMNS c JOIN ALL_TABLES t ON t.OWNER = c.OWNER AND t.TABLE_NAME = c.TABLE_NAME LEFT JOIN ALL_COL_COMMENTS cm ON cm.OWNER = c.OWNER AND cm.TABLE_NAME = c.TABLE_NAME AND cm.COLUMN_NAME = c.COLUMN_NAME WHERE c.OWNER = :owner ORDER BY c.TABLE_NAME, c.COLUMN_ID"; using var conn = new OracleConnection(connStr); conn.Open(); using var cmd = new OracleCommand(sql, conn); cmd.BindByName = true; // 必须开,否则按位置绑定 cmd.Parameters.Add(":owner", owner.ToUpperInvariant()); using var reader = cmd.ExecuteReader(); while (reader.Read()) { var table = reader.GetString(0); var col = new ColumnMeta { TableName = table, ColumnId = reader.GetInt32(1), ColumnName = reader.GetString(2), DataType = reader.GetString(3), DataPrecision = reader.IsDBNull(4) ? null : reader.GetInt32(4), DataScale = reader.IsDBNull(5) ? null : reader.GetInt32(5), CharLength = reader.IsDBNull(6) ? null : reader.GetInt32(6), IsNullable = reader.GetString(7) == "Y", Comment = reader.IsDBNull(8) ? null : reader.GetString(8) }; // 按 TableName 聚合到字典 tables.GetOrAdd(table).Columns.Add(col); }

逻辑说明几点。BindByName = true是 ODP.NET 最容易忘的一行,默认是按位置绑定,参数名写:owner但传进去的值会按顺序对,参数一多必然串位。owner.ToUpperInvariant()是因为 Oracle 里未加引号的标识符默认大写,用小写查ALL_TAB_COLUMNS会一条都查不到,这个现象特别像“数据库连错了”。reader.IsDBNull判断不能省,DATA_PRECISION在字符串列上是NULL,直接GetInt32会抛InvalidCastException,报错信息还只告诉你“无效的转换”,看不出是哪一列。

元数据查完以后,主键再单独跑一次上面那条ALL_CONSTRAINTS的 SQL,把结果按TABLE_NAME塞进TableMeta.PrimaryKeys,顺序就是POSITION。

3.3 模板拼接与落盘

到这一步数据都齐了,剩下的就是把TableMeta变成字符串。别上 Razor、别上 T4,字符串插值加StringBuilder完全够用,读起来也直观。

private static string BuildEntity(TableMeta t, string ns) { var sb = new StringBuilder(); sb.AppendLine("using System;"); sb.AppendLine("using System.ComponentModel.DataAnnotations;"); sb.AppendLine("using System.ComponentModel.DataAnnotations.Schema;"); sb.AppendLine(); sb.AppendLine($"namespace {ns};"); sb.AppendLine(); if (!string.IsNullOrEmpty(t.TableComment)) sb.AppendLine($"/// <summary>{t.TableComment}</summary>"); sb.AppendLine($"[Table(\"{t.TableName}\")]"); sb.AppendLine($"public partial class {ToPascal(t.TableName)}"); sb.AppendLine("{"); foreach (var c in t.Columns.OrderBy(x => x.ColumnId)) { var prop = ToPascal(c.ColumnName); var type = MapType(c); var nullable = c.IsNullable && IsValueType(type) ? "?" : ""; sb.AppendLine($" /// <summary>{c.Comment}</summary>"); sb.AppendLine($" [Column(\"{c.ColumnName}\")]"); sb.AppendLine($" public {type}{nullable} {prop} {{ get; set; }}"); sb.AppendLine(); } sb.AppendLine("}"); return sb.ToString(); } // 落盘:一张表一个文件,文件名用类名 var dir = Path.Combine(AppContext.BaseDirectory, "Entities"); Directory.CreateDirectory(dir); foreach (var t in tables.Values) { var code = BuildEntity(t, "Demo.Domain.Entities"); File.WriteAllText(Path.Combine(dir, ToPascal(t.TableName) + ".cs"), code, new UTF8Encoding(false)); // 不加 BOM,避免 Git 首次 diff 全红 }

参数和逻辑说明。ToPascal是自己写的命名转换,把T_ORD_MST转成TOrdMst这种形式,规则要统一,别一会儿去前缀一会儿不去——生成器最怕“看起来对”,改一个规则全量重生成一次,diff 一看就知道有没有波及。UTF8Encoding(false)显式不带 BOM,Windows 上默认File.WriteAllText的Encoding.UTF8是带 BOM 的,第一次提交到 Git 会发现整个文件都在变,血泪经验。partial class从一开始就加上,后面要写扩展方法、加计算属性、加[NotMapped],都放到另一个文件里,重新生成不会覆盖。

命名规则这里多说一句:表名去掉T_前缀、下划线转驼峰,这套规则在国内项目里命中率很高,但不是所有库都这样。ToPascal最好做成可配置的委托,Func<string, string>,换项目改一个 lambda 就行,不要写死在生成器里。

4. 不想手写模板:EF Core Scaffold 和轻量 ORM 的 DbFirst 怎么选

自己写生成器的前提是规则多、需要定制。如果项目是个标准的新库,表名规范、注释齐全、没有奇怪的NUMBER(1),直接用现成脚手架更省事。这一章把三条路线的取舍摆出来,再说 EF Core Scaffold 的具体命令和参数。

4.1 三条路线的取舍

维度自写 ODP.NET 生成器EF Core Scaffold轻量 ORM 的 DbFirst
上手成本两百行代码,一天一条命令,十分钟一条命令,半小时配连接
定制能力完全可控靠--data-annotations和手改中等,靠模板或配置
生成产物纯 POCOPOCO + DbContext + 关系导航POCO + 少量特性
关系导航要自己写自动生成外键导航部分支持
复合主键自己拼HasKey自动处理视框架而定
重复生成覆盖靠partial规避同左,官方建议用 partial同左
适合场景存量库、规则复杂新项目、Code First 为主只要实体不要上下文

EF Core 的优势是外键导航自动生成,一张订单明细表会顺带出一个Order导航属性,写查询的时候省不少事。代价是数据库里如果有循环外键、自引用外键,脚手架生成的导航属性会互相打架,OnModelCreating里一堆HasOne/WithMany要手工修。

轻量 ORM 的 DbFirst 只需实体、不要DbContext的场景更合适,配置简单,生成出来就是干净的 POCO,缺点是对复合主键和注释的支持看版本,有的版本注释压根不生成。

自写生成器的价值在于“规则能落地”:命名转换、类型覆盖、注释格式、字段顺序,全都可以按团队规范来。它的成本也在这——规则是你在维护,库结构一变,规则要跟着调。

4.2 EF Core Scaffold 实操

.NET 8 项目,先装两个包,版本要跟着 .NET 版本走:

dotnet add package Oracle.EntityFrameworkCore --version 8.23.50 dotnet add package Microsoft.EntityFrameworkCore.Design --version 8.0.0

版本号不是随便写的,Oracle.EntityFrameworkCore的大版本要和Microsoft.EntityFrameworkCore对齐,8.x 配 .NET 8,装错版本运行时会报Method not found,错误堆栈深得看不出原因。

脚手架的完整命令:

dotnet ef dbcontext scaffold \ "User Id=app_ro;Password=***;Data Source=10.0.0.12:1521/orclpdb" \ Oracle.EntityFrameworkCore \ --output-dir Models \ --context-dir Data \ --context DemoDbContext \ --schema APP \ --no-onconfiguring \ --use-database-names \ --data-annotations \ --force

参数逐个说。--output-dir是实体目录,--context-dir是DbContext目录,两个分开更清楚。--schema一定要指定,不指定会把SYS、SYSTEM下的系统表也扫进来,生成几百个莫名其妙的实体。--no-onconfiguring让生成的DbContext不带硬编码连接串,连接串从配置文件读,这条不加,密码就进 Git 了。--use-database-names表示类名和属性名照抄数据库原始名字,好处是绝对不会错,坏处是你会得到public string BILL_NO { get; set; }这种全大写属性。我的习惯是首次生成加上这个参数确认映射,确认完去掉它重新生成,让脚手架做 Pascal 转换,再手工核对一遍容易错的缩写词。

--data-annotations生成[Required]、[MaxLength]这种特性,不用Fluent API的项目用它,代码看着直观。--force是覆盖已有文件,第一次生成别加,先看看产物对不对。

生成完必须做一件事:把DbContext里的OnModelCreating逐个过一遍。Oracle 的NUMBER(1)会被映射成bool或者byte,取决于版本,看到不对的列直接在OnModelCreating里加HasConversion,或者回退到 3.1、3.2 节那套自写映射。

提示:脚手架重复执行会覆盖Models下的所有文件,手工加的属性和特性会丢。第一次生成完就把文件改成partial,扩展逻辑单开一个Entities.Ext.cs,后面爱怎么重生成怎么重生成。

5. 避坑与排查:生成出来的实体和库里的表对不上

生成器跑通不代表产物能用,真正花时间的是“跑通了但结果不对”。下面五条是这类项目里出现频率最高的,每条都按现象、原因、解决写。

5.1 现象:一条数据都查不出来,日志里是 ORA-00942

原因:Oracle 里未加引号的标识符默认大写,生成器如果拼了select * from t_ord_mst,Oracle 会去查T_ORD_MST,表在APP用户下、连接串是APP用户,理论上能查到;一旦跨 schema 或者表名建的时候加过双引号,大小写就对不上了。另一种情况是 EF 配了--use-database-names,实体上是[Table("t_ord_mst")],Oracle 会当成带引号的小写表名去找。

解决:先确认表实际大小写,SELECT TABLE_NAME FROM ALL_TABLES WHERE OWNER = 'APP' AND UPPER(TABLE_NAME) = 'T_ORD_MST',拿到的值才是真名。生成器里所有表名、列名统一ToUpperInvariant();EF 实体的[Table]特性去掉--use-database-names让它自己转,或者手工改成大写。别在连接串上想办法,大小写不是连接层的问题。

5.2 现象:金额字段读出来少两位小数,或者精度对不上

原因:NUMBER(18,2)被映射成了double或者long。double有浮点误差,1.005存进去取出来可能变成1.00499999;映射成long直接把小数位截掉。还有人把NUMBER映射成int,19 位的 ID 直接溢出成负数。

解决:DATA_SCALE > 0一律decimal,DATA_PRECISION >= 19一律decimal,这两条规则写死在映射函数里,不留例外。已经生成的实体要全量扫一遍,用正则找public double Amount这类写法,逐个改。decimal的精度用[Column(TypeName = "NUMBER(18,2)")]标出来,EF 生成迁移或者拼 SQL 时会用到。

5.3 现象:插入时报“无法将 NULL 插入列”,但代码里明明赋了值

原因:NULLABLE = 'N'的列被映射成了int之外的引用类型,或者反过来——NULLABLE = 'Y'的列映射成了int,EF 在插入时判断0是默认值就跳过,Oracle 里没有默认值直接报错。还有一种:DATA_DEFAULT有值但生成器没处理,实体上没有初始化表达式,new出来的对象字段是null。

解决:NULLABLE = 'Y'且 C# 是值类型的,一律加?,DateTime?、decimal?、int?都算。引用类型.NET 6以上开了可空引用类型,string?也要显式标出来,不然编译器警告一片。DATA_DEFAULT读出来是SYSDATE、0、'N'这种表达式,别往属性初始化器里塞,看不懂的默认值交给数据库,实体上标[DatabaseGenerated(DatabaseGeneratedOption.Computed)]就行。

5.4 现象:EF 启动报 “The entity type requires a primary key”

原因:主键查询只查了CONSTRAINT_TYPE = 'P',但有些表的主键是唯一索引 +NOT NULL建出来的,没有主键约束,ALL_CONSTRAINTS里查不到。或者表压根没主键,这在老系统里不罕见,日志表、中间表经常这样。

解决:先补一查CONSTRAINT_TYPE IN ('P','U'),'U'是唯一约束,很多老库拿它当主键用。查完还是没有,就在生成器里加规则:找不到主键的表,把COLUMN_ID最小的NOT NULL列当逻辑主键,同时在实体上标注[Key],并在生成日志里打一行警告,让人知道这是猜的。猜错了比启动不了好排查,至少业务能跑起来。

5.5 现象:第二次生成把手工改的代码全冲掉了

原因:生成器全量覆盖,没有增量判断,也没有partial拆分。这个坑几乎每个自研生成器都会踩一次,第一次覆盖之后才发现改了两天的计算属性没了。

解决:从第一天就分两个文件,Xxx.cs是生成产物,头部加一行// <auto-generated />,Xxx.Ext.cs是手写扩展,生成器永远不碰它。生成前做一次内容比对,内容没变的文件跳过不写,File.GetLastWriteTime和内容哈希都行,内容哈希更准;变了的写之前先备份到.bak目录,跑完看一眼 diff 再删。

6. 让生成器能长期用:增量、校验和一个 dry-run 开关

生成器写出来只花一天,能不能用半年,全看后面这几件事有没有做。

第一件是生成范围的收敛。全库生成看着爽,但一个 200 张表的库,每次重生成都产生几十个无关文件的 diff,代码评审根本看不过来。我的做法是维护一个tables.json白名单,只生成业务真正用到的表,白名单之外的表要加,走一次评审。文件结构大概这样:

{ "owner": "APP", "namespace": "Demo.Domain.Entities", "tables": [ { "name": "T_ORD_MST", "class": "OrderMaster" }, { "name": "T_ORD_DTL", "class": "OrderDetail" } ], "typeOverrides": { "T_ORD_MST.STATUS": "OrderStatus" } }

typeOverrides这个字典是这套方案里最值钱的部分。数据库里那些说不清道不明的NUMBER(1),靠全局规则永远映射不对,用表名加列名精确覆盖,一次配好长期有效。配错的时候生成器要报错退出,而不是默默生成一个错的类型——生成器最怕的不是报错,是安静地产出垃圾。

第二件是生成前的校验。每次跑之前先查一遍白名单里的表在库里还在不在,字段有没有改名。改名是最容易出事的,列名从AMT变成AMOUNT,重新生成后旧属性消失,编译期就会报错,但如果项目里还有反射或者 Dapper 的裸 SQL 在用AMT,那就是运行期才炸。生成器里加一段:

// 生成前比对:库里没有的表直接报错,字段变更打警告 var missing = config.Tables .Where(t => !dbTables.ContainsKey(t.Name)) .Select(t => t.Name).ToList(); if (missing.Any()) throw new InvalidOperationException($"以下表在库中不存在,请先更新白名单:{string.Join(",", missing)}");

第三件是--dry-run开关。加上这个参数只打印将要写入的文件清单和变更行数,不落盘。接到 CI 里,每次合并请求跑一次,输出“本次将变更 3 个文件、新增 2 个字段”,评审的人一眼就能看出这次数据库改动的影响面。没有这个开关,生成器就是个黑匣子,谁也不敢随便跑。

最后说一个习惯。前面那个八十张表的项目,最后生成的实体有六张表的字段类型是我手工改过的,原因都是NUMBER(1)和NUMBER(10,2)被错误映射。从那以后我每次接新库,第一件事不是写生成器,而是先跑一条GROUP BY DATA_TYPE, DATA_PRECISION, DATA_SCALE的统计,把库里所有类型组合列出来,对这几十行结果逐行确认映射规则,确认完再动手。这个顺序反了,就会变成“生成—发现不对—改映射—再生成”的循环,来回几轮下来,人比手写还累。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询