Missing value for column 'body'
主线位置:查询主线的最后一步,
Model::from_query_result。DeriveEntityModel生成的这个函数先把每一列都读成Option,最后再逐个ok_or_else;任何一列得到None,就报这一句。生成代码的全貌见 DeriveEntityModel 与 dense 格式,TryGetable怎么把 NULL 和缺列都变成None见列与值。
flowchart TB
row["QueryResult 一行"] --> rd["每列 try_get_nullable Option T"]
rd --> n1["列是 NULL"]
rd --> n2["列不在结果里"]
rd --> ok["列有值"]
n1 --> none["None"]
n2 --> none
none --> fld{"字段是 Option 吗"}
fld -->|是| fine["字段 = None"]
fld -->|否| err["Missing value for column"]
本页实验在 M2 Max、macOS、rustc 1.99.0、Postgres 16.14 上运行,依赖 sea-orm = "=2.0.4"(对照组 =1.1.20)。表:
CREATE TABLE post (id serial PRIMARY KEY, title text NOT NULL, body text);
INSERT INTO post (title, body) VALUES ('hello', 'first'), ('draft', NULL);
post 实体把 body 写成 Option<String>;另一个 strict 实体映射同一张表,body 写成 String。两个都是 #[sea_orm::model] 的 dense 写法。
现象
src/bin/missing.rs:
let r = strict::Entity::find_by_id(2).one(&db).await;
println!("1 Debug : {r:?}");
show("1 NULL into String ", r);
show("2 select_only(id) ", post::Entity::find().select_only().column(post::Column::Id).one(&db).await);
show("3 select_except([title]) ", post::Entity::find().select_except([post::Column::Title]).one(&db).await);
1 Debug : Err(Type("Missing value for column 'body'"))
1 NULL into String : Type Error: Missing value for column 'body'
2 select_only(id) : Type Error: Missing value for column 'title'
3 select_except([title]) : Type Error: Missing value for column 'title'
三种原因,一句报错:
- 1:id 2 的
body是 NULL,字段却是String。 - 2:
select_only().column(Id)只选了id,title不在结果里。 - 3:
select_except([Title])是 2.0.4 新加的方法,选除了title以外的列,title同样不在结果里。
sqlx 日志里 2 和 3 实际发出的语句:
INFO sqlx::query: summary="SELECT \"post\".\"id\" FROM \"post\" …" db.statement="\n\nSELECT \"post\".\"id\" FROM \"post\" LIMIT $1\n" rows_affected=1 rows_returned=1 elapsed=884.167µs elapsed_secs=0.000884167
INFO sqlx::query: summary="SELECT \"post\".\"id\", \"post\".\"body\" FROM …" db.statement="\n\nSELECT \"post\".\"id\", \"post\".\"body\" FROM \"post\" LIMIT $1\n" rows_affected=1 rows_returned=1 elapsed=846.625µs elapsed_secs=0.000846625
语句都执行成功了,各返回一行,错误出在解码。报错是 DbErr::Type,不是 DbErr::Query,也就没有 SQLSTATE 可查。
出处
impl_from_query_result 给每个不是 ignore 的字段生成一个读取和一个解包:
let reader = quote! {
let #field_ident =
#row.try_get_nullable::<Option<#field_type>>(
#pre,
sea_orm::IdenStatic::as_str(
&<<Self as sea_orm::ModelTrait>::Entity
as sea_orm::entity::EntityTrait>::Column::#column_ident
).into()
)?;
};
let unwrapper = quote! {
#field_ident: #field_ident.ok_or_else(|| sea_orm::DbErr::Type(
format!(
"Missing value for column '{}'",
sea_orm::IdenStatic::as_str(
&<<Self as sea_orm::ModelTrait>::Entity
as sea_orm::entity::EntityTrait>::Column::#column_ident
)
)
))?
};
读的类型是 Option<#field_type>。字段是 String 时读的是 Option<String>,字段本来就是 Option<String> 时读的是 Option<Option<String>>。Option<T> 的 TryGetable 把 TryGetError::Null 和 sqlx 的 ColumnNotFound 都变成 Ok(None),所以「列是 NULL」和「列根本没选」在这里已经分不开了,最后统一由 ok_or_else 报出来。外面那层 Option 是为 LEFT JOIN 的嵌套 Option<Model> 加的「整行全是 NULL 就当没有这一行」的检查,来历见列与值的冷知识(#2845,2.0.0-rc.29)。
报错文字里只有列名,没有表名和行号。Column::#column_ident 的 as_str() 是列名本身,所以带 #[sea_orm(column_name = "..")] 的字段报的是数据库里的列名,不是 Rust 字段名。多个字段同时缺值时,报的是字段顺序里的第一个。
select_except 只改 SELECT 列表,Selector 的类型仍然是整个 Model,它的文档注释直接写着这个后果:
/// Rows still hydrate into the full `Model`, so only `Option<_>` columns can be
/// excluded. Excluding a non-nullable column results in a runtime error,
/// `Missing value for column`. Excluding every column leaves nothing to select
/// and produces invalid SQL.
select_except 发布时就带着这条限制#3211(首次发布于 2.0.4)加了 Select::select_except,2.0.4 的 release notes 说它是「the inverse of select_only / column」。PR 说明把这个坑写成了已知行为,而不是要修的问题:「Rows still hydrate into the full Model: only Option<_> columns can be excluded; a non-nullable column fails with Missing value for column」,并且加了「non-nullable failure」的集成测试。release notes 里的示例配的是一个 DerivePartialModel(PostPreview { id, title }),也就是下面修法里的第二条路。
本站用的 1.1.20 两种情况各报各的(exp-seaorm-pit1-old/src/bin/missing_old.rs,同一张表,展开写法的实体):
1 NULL into String: Err(Type("A null value was encountered while decoding \"body\""))
Display : Type Error: A null value was encountered while decoding "body"
2 select_only(id) : Err(Query(SqlxError(ColumnNotFound("title"))))
Display : Query Error: no column found for name: title
1.1.20 的 impl_from_query_result 对每个字段直接生成 row.try_get(pre, 列名)?,按字段的真实类型读,NULL 是 TryGetError::Null,缺列是 sqlx 的 ColumnNotFound。升级到 2.0 之后,日志里按这两句文字做的告警或测试断言会失效。
修法
按原因分开处理。同一个程序的后半段:
show("4 select_except([body]) ", post::Entity::find().select_except([post::Column::Body]).one(&db).await);
show("5 Option body, id 2 ", post::Entity::find_by_id(2).one(&db).await);
show(
"6 select_only + into_tuple",
post::Entity::find().select_only().column(post::Column::Id).into_tuple::<i32>().all(&db).await,
);
4 select_except([body]) : Ok(Some(Model { id: 1, title: "hello", body: None }))
5 Option body, id 2 : Ok(Some(Model { id: 2, title: "draft", body: None }))
6 select_only + into_tuple: Ok([1, 2])
- 列确实可空(1):字段写成
Option<T>,见 5。不想改实体,就在 SQL 里保证不出 NULL,或者换下面的读法。 - 只想要部分列(2、3):不要再读成实体的
Model。按位置读用into_tuple(6),按名字读用#[derive(DerivePartialModel)]让SELECT列表从结构体的字段推出来,两种做法见部分列与自定义结果。那一页还有一张表:同一个缺列,自己派生的FromQueryResult报的是ColumnNotFound,不是本页这句。 - 4 是
select_except唯一能用的情况:被排除的是Option列。注意 id 1 的body在库里是'first',读出来却是None:排除掉的列和 NULL 列在Model里长得一样,把这样的Model转成ActiveModel再写回去之前要想清楚。
怎么确认没中招
- 看到这句报错时,先打开 sqlx 日志(
sqlx_logging(true)加sqlx=info)看那条语句的SELECT列表里有没有报错的列:没有就是 2、3,有就是 1。 - 对每个实体,比较字段的可空性和库里的
is_nullable。ColumnTrait::def().is_null()是实体这一侧(字段是Option时为true),information_schema.columns是库这一侧(src/bin/nullcheck.rs,对strict实体):
for col in strict::Column::iter() {
// ... SELECT is_nullable = 'YES' FROM information_schema.columns WHERE ... column_name = $2
let rust_null = col.def().is_null();
let mark = if db_null && !rust_null { " <-- nullable in db, not Option in Rust" } else { "" };
println!("{table}.{:<6} db nullable={db_null:<5} entity nullable={rust_null:<5}{mark}", col.as_str());
}
post.id db nullable=false entity nullable=false
post.title db nullable=false entity nullable=false
post.body db nullable=true entity nullable=false <-- nullable in db, not Option in Rust
这个检查可以放进连测试库的集成测试,迁移改了列的约束而实体没跟着改时就会报出来。它管不了 LEFT JOIN 补出来的 NULL 和表达式列,那些要按可空处理。
- 本站的
select_only用法(admin/src/server.rs的两个GROUP BY、forum/src/server.rs的标签统计)都接着into_tuple或自定义的FromQueryResult,不读成实体Model,不受 2、3 影响。
小结
- 2.0 的实体
Model先把每列读成Option,NULL 和缺列都变成None,最后报Missing value for column '..'(DbErr::Type);三种原因:NULL 落进非Option字段、select_only漏了列、select_except排除了非空列。 - 1.1.20 分别报
A null value was encountered while decoding "body"和no column found for name: title,升级后文字变了。 - 可空列写
Option<T>;只要部分列就用into_tuple或DerivePartialModel,不要读成整个Model。 select_except只能排除Option列,排除掉的列读出来是None,和真的 NULL 分不开。- 用
def().is_null()对照information_schema.columns.is_nullable,可以在测试里提前发现实体和表的可空性不一致。