1. EF6 写 localdb 中文变问号,问题到底出在哪一层
如果你在用 EF6 配合 localdb 做本地开发,遇到一个很典型的现象:直接在 SSMS 或 VS 的 SQL 查询窗口里insert into一条中文数据,查出来是正常的;但换成 ASP.NET 里用 EF6 的SaveChanges()写进去,中文就全变成问号???。这个现象我第一次碰到时也懵了很久,因为同一张表、同一个字段,SQL 手写没问题,代码写就乱码,直觉上会怀疑是 C# 字符串编码问题,但实际排查下来,根子往往在数据库的排序规则(Collation)上。
localdb 是 SQL Server Express 的一个轻量运行模式,默认实例创建时用的排序规则通常是SQL_Latin1_General_CP1_CI_AS。这个排序规则对应的代码页是 Latin1,对非 Unicode 字符类型(varchar、char、text)来说,它无法正确存储中文,写入时会被替换成问号。而你在查询窗口里手写 SQL 能显示正常,很可能是因为你写的是N'测试'这种 Unicode 字面量,或者查询工具做了额外处理,掩盖了底层存储已经损坏的事实。
EF6 默认把string映射成nvarchar,理论上应该走 Unicode 通道,不受排序规则影响。但问题在于:如果数据库或列的排序规则是 Latin1,而 EF6 在生成参数时用了非 Unicode 的SqlDbType.VarChar,或者迁移脚本里字段被定义成了varchar,中文就会在写入那一刻被转成问号。所以这条链路要分三层看:连接字符串有没有指定字符集相关参数、数据库和列的排序规则是不是中文、Code First 迁移生成的字段类型对不对。
适合读这篇的人:正在用 EF6 + localdb 做本地开发、被中文乱码卡住、想按步骤定位到底哪一层出问题的同学。下面我会给出可复制的App.config/Web.config骨架、一段最小插入验证代码,以及逐步验证动作,帮你把乱码发生的位置钉死。
2. 前置准备:TaoToken 与 localdb 环境确认
在动手改配置之前,先把环境确认清楚。localdb 的实例名一般是(localdb)\MSSQLLocalDB,你可以用sqllocaldb info命令查看当前实例状态。如果你在排查过程中需要对照 EF6 的官方行为、或者想快速验证某段 SQL 在 Unicode 下的表现,可以借助 TaoToken 的模型对话能力来辅助理解报错和生成排查脚本,它支持多模型切换,适合边查边试。
TaoToken 的接入入口在这里:
- 官网:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
- API 地址:https://taotoken.net/api
- 模型对话:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite
- 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite
- API Keys:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite
需要说明的是,TaoToken 在这里的角色是帮你查资料、验证思路、生成排查 SQL 的辅助工具,不是替代你本地数据库操作的东西。localdb 的排序规则修改、EF6 的配置调整,还是要在你自己的开发环境里完成。
确认环境时,重点看三件事:当前 localdb 实例的排序规则是什么、你的 EF6 上下文连的是哪个数据库、Code First 迁移生成的字段类型是nvarchar还是varchar。这三件事决定了乱码发生在哪一层。
3. 可复制配置:App.config / Web.config 骨架与排序规则修正
3.1 连接字符串骨架
EF6 的连接字符串里,providerName必须是System.Data.SqlClient,AttachDbFilename指向你的 mdf 文件。很多人乱码的根源之一,是连接字符串里加了Charset之类的参数,但 SqlClient 并不认这个参数,真正影响字符存储的是数据库排序规则,不是连接字符串。下面是一个可用的骨架:
<configuration> <configSections> <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" /> </configSections> <connectionStrings> <add name="MyDbContext" connectionString="Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=MyDb;Integrated Security=True;MultipleActiveResultSets=True" providerName="System.Data.SqlClient" /> </connectionStrings> <entityFramework> <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework"> <parameters> <parameter value="mssqllocaldb" /> </parameters> </defaultConnectionFactory> <providers> <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" /> </providers> </entityFramework> </configuration>注意这里没有加任何Charset参数,因为 SqlClient 的字符处理由数据库排序规则和字段类型决定。如果你看到网上有人让你在连接字符串里加Charset=utf8,那是 MySQL 的写法,放到 SqlClient 上无效,反而会误导排查方向。
3.2 修改 localdb 数据库排序规则
localdb 默认排序规则不是中文,这是乱码的核心原因。你需要把数据库排序规则改成Chinese_PRC_CI_AS或Chinese_PRC_CS_AS。改之前要先把数据库切到单用户模式,否则会报「数据库正在使用」的错误。下面这段 SQL 可以直接在 SSMS 或 VS 的查询窗口里执行:
DECLARE @database NVARCHAR(100) DECLARE @sql NVARCHAR(500) DECLARE tmpCur CURSOR FOR SELECT DB_NAME() OPEN tmpCur FETCH NEXT FROM tmpCur INTO @database SELECT @sql = 'ALTER DATABASE [' + @database + '] SET SINGLE_USER WITH ROLLBACK IMMEDIATE' EXEC(@sql) SELECT @sql = 'ALTER DATABASE [' + @database + '] COLLATE Chinese_PRC_CI_AS' EXEC(@sql) SELECT @sql = 'ALTER DATABASE [' + @database + '] SET MULTI_USER' EXEC(@sql) CLOSE tmpCur DEALLOCATE tmpCur执行完之后,用SELECT DATABASEPROPERTYEX('MyDb', 'Collation')确认排序规则已经变成Chinese_PRC_CI_AS。这一步只改数据库级排序规则,已有的列如果之前是varchar,还需要单独改列,或者干脆重建表。
3.3 Code First 迁移里字段类型要盯紧
EF6 默认把string映射成nvarchar,但如果你在OnModelCreating里用了HasColumnType("varchar"),或者迁移脚本被手动改过,字段就会变成varchar,中文照样乱码。检查迁移文件里的CreateTable语句,确认中文字段是nvarchar而不是varchar:
public partial class Init : DbMigration { public override void Up() { CreateTable( "dbo.Articles", c => new { Id = c.Int(nullable: false, identity: true), Title = c.String(maxLength: 200, unicode: true), Content = c.String(unicode: true), }) .PrimaryKey(t => t.Id); } }unicode: true是关键,它会让 EF6 生成nvarchar。如果你之前已经生成了varchar列,改完模型后重新生成迁移,或者手动执行ALTER TABLE Articles ALTER COLUMN Title NVARCHAR(200)。
4. 验证请求:最小插入代码与成功结果
配置改完之后,用一段最小代码验证中文能不能正确写入。下面是一个控制台或 ASP.NET 里都能用的片段:
using (var db = new MyDbContext()) { var article = new Article { Title = "中文测试标题", Content = "这是一段中文内容,用来验证 EF6 写入 localdb 是否乱码。" }; db.Articles.Add(article); db.SaveChanges(); var saved = db.Articles.OrderByDescending(a => a.Id).FirstOrDefault(); Console.WriteLine("写入后读回:" + saved.Title); Console.WriteLine("写入后读回:" + saved.Content); }运行后,如果控制台输出的是正常中文,说明整条链路已经通了。如果还是问号,就继续往下看排查清单。
你也可以在数据库里直接查:
SELECT TOP 5 Id, Title, Content FROM Articles ORDER BY Id DESC如果这里显示正常,但 C# 读回来是问号,那问题在读取端的编码处理;如果这里就是问号,那问题在写入端,回到排序规则和字段类型上继续查。
5. 本篇常见错排查清单
5.1 改了数据库排序规则,但列还是 varchar
数据库级排序规则改了,不代表已有列会自动变。varchar列的排序规则是列级属性,需要单独改。用下面这段 SQL 查出所有非 Unicode 的中文字段:
SELECT t.name AS TableName, c.name AS ColumnName, c.max_length, c.collation_name FROM sys.columns c JOIN sys.tables t ON c.object_id = t.object_id JOIN sys.types ty ON c.user_type_id = ty.user_type_id WHERE ty.name IN ('varchar', 'char', 'text') ORDER BY t.name, c.name查到之后,逐个ALTER TABLE ... ALTER COLUMN ... NVARCHAR(...)。
5.2 连接字符串里加了无效的 Charset 参数
前面提过,SqlClient 不认Charset。如果你从 MySQL 或别的数据库迁移过来,习惯性加了Charset=utf8,它不会报错,但也不会生效,反而让你以为已经处理了编码问题。删掉它,把注意力放回排序规则和字段类型。
5.3 迁移没重新生成,模型改了但数据库没变
Code First 里改了模型属性,如果没有Add-Migration和Update-Database,数据库结构不会变。很多人改了unicode: true却忘了更新迁移,结果字段还是旧的varchar。用Update-Database -Verbose看实际执行的 SQL,确认ALTER COLUMN有没有跑。
5.4 localdb 实例被多个项目共用,排序规则互相覆盖
localdb 的(localdb)\MSSQLLocalDB是共享实例,如果你有多个项目连同一个实例、同一个数据库名,排序规则可能被别的项目改回去。排查时先确认SELECT DB_NAME()和DATABASEPROPERTYEX的结果,别改错了库。
5.5 读取端用了非 Unicode 的 DataReader
极少数情况下,写入是nvarchar,但读取时用了GetString之外的GetSqlString或手动转varchar,也会出现问号。检查你的查询有没有CAST或CONVERT到varchar,有的话去掉。
6. 继续排查与工具入口
乱码排查的核心思路是分层定位:先确认数据库排序规则,再确认列类型,最后确认 EF6 映射和读取方式。这三层里任何一层用了非 Unicode 通道,中文就会在那一层被替换成问号。改完排序规则后,记得重新生成迁移并更新数据库,别只改一半。
如果你在排查过程中需要快速验证某段 SQL 的行为、或者想让模型帮你分析 EF6 生成的 SQL 日志,可以用 TaoToken 的模型对话来辅助:
- 模型对话:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite
- 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite
- API Keys:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite
如果你在做长期的 EF6 项目维护、需要频繁生成迁移脚本和排查 SQL 日志,可以考虑 Coding Plan,把这类重复性的排查工作交给模型辅助:
- Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite
最后提醒一句:改排序规则前先备份 mdf 文件,localdb 虽然轻量,但改坏了重建也麻烦。把Chinese_PRC_CI_AS设对、字段设成nvarchar、迁移更新到位,EF6 写中文就不会再变问号了。