如何在sqlx中正确使用PostGIS的Geography类型实现位置数据的插入与查询
我太懂你这种卡壳的感觉了——想用sqlx操作PostGIS的Geography类型,结果不管是查询还是插入都因为类型不匹配报错,尤其是Geography和Geometry在sqlx的类型映射里默认不兼容对吧?别着急,我来一步步帮你把代码改对。
首先先分析核心问题:PostGIS的geometry和geography是两种不同的SQL类型,sqlx和geozero的默认集成是把WKB类型和geometry绑定的,但你的列是geography,所以出现了“类型不兼容”的错误。我们需要手动处理字节数组的解码/编码,绕开sqlx的类型绑定限制。
第一步:修正查询的FromRow实现
你原来的代码里直接用row.try_get("coordinate")获取wkb::Decode<Geometry<f64>>,但sqlx会把数据库里的geography类型和这个Rust类型不匹配。解决方法是直接取原始字节数组,自己用geozero解码:
use geo::{Coord, Point}; use geo_types::Geometry; use geozero::wkb; use sqlx::{postgres::PgRow, Error, FromRow}; use uuid::Uuid; use serde::Serialize; // 确保引入serde的Serialize trait #[derive(Serialize, Debug)] pub struct Location { pub id: Uuid, pub coordinate: Coord<f64>, } impl FromRow<'_, PgRow> for Location { fn from_row(row: &PgRow) -> Result<Self, Error> { // 1. 直接获取geography列的原始字节数组(WKB格式) let wkb_bytes: Vec<u8> = row.try_get("coordinate")?; // 2. 手动用geozero解码字节数组为Geometry let decoder = wkb::Decode::from_bytes(&wkb_bytes) .map_err(|e| Error::ColumnDecode { index: "coordinate".to_string(), source: Box::new(e), })?; // 3. 转成Point和Coord,替换unwrap为严谨的错误处理 let geometry = decoder.geometry.ok_or_else(|| Error::ColumnDecode { index: "coordinate".to_string(), source: Box::new(std::io::Error::new(std::io::ErrorKind::InvalidData, "Empty geometry")), })?; let point = Point::try_from(geometry) .map_err(|e| Error::ColumnDecode { index: "coordinate".to_string(), source: Box::new(e), })?; let coord: Coord<f64> = Coord::from(point); Ok(Self { id: row.try_get("id")?, // 用列名替代索引,更健壮 coordinate: coord, }) } }
这里的关键改动:不再依赖sqlx自动映射geography类型,而是直接取原始字节数组自行解码,绕开了sqlx的类型绑定限制,同时替换了不安全的unwrap,增加了严谨的错误处理。
第二步:修正插入逻辑
你原来的插入语句用了ST_SetSRID($2::geometry, 4326)::geography,其实可以更直接。下面提供两种可靠的方案:
方案1:用PostGIS函数构造Geography(更直观)
直接传递经纬度参数,用PostGIS的ST_MakePoint和ST_SetSRID构造Geography,代码更易读:
use geo::Point; use sqlx::{postgres::PgPool, Error, Postgres}; use uuid::Uuid; pub async fn create_location( db_pool: &PgPool, coordinate: Point<f64>, ) -> Result<Location, Error> { let uuid = Uuid::new_v4(); // 用PostGIS函数直接构造Geography类型 let location = sqlx::query_as!( Location, r#" INSERT INTO location (id, coordinate) VALUES ($1, ST_SetSRID(ST_MakePoint($2, $3), 4326)::geography) RETURNING * "#, uuid, coordinate.x(), // 传入经度 coordinate.y() // 传入纬度 ) .fetch_one(db_pool) .await?; Ok(location) }
方案2:传递WKB字节数组转Geography
如果你偏好使用WKB传递数据,可以把geo::Point转成WKB字节数组,再在SQL中转为geography:
pub async fn create_location( db_pool: &PgPool, coordinate: Point<f64>, ) -> Result<Location, Error> { let uuid = Uuid::new_v4(); // 将Point转为Geometry,再编码为WKB字节数组 let geometry: geo_types::Geometry<f64> = coordinate.into(); let wkb_bytes = geozero::wkb::Encode(geometry).to_vec() .map_err(|e| Error::Encode(Box::new(e)))?; let location = sqlx::query_as!( Location, r#" INSERT INTO location (id, coordinate) VALUES ($1, $2::geography) RETURNING * "#, uuid, wkb_bytes ) .fetch_one(db_pool) .await?; Ok(location) }
注意:你原来的代码里有多余的.bind(&collection_uuid),这应该是笔误,我已经在修正代码中移除了。
为什么原来的代码会报错?
你原来的查询代码中,row.try_get("coordinate")试图把geography类型列映射成wkb::Decode<Geometry>,但sqlx的类型映射规则里,wkb::Decode<Geometry>对应的SQL类型是geometry,和你的geography列类型不匹配,因此抛出了类型错误。通过直接取字节数组自行解码,就完美绕开了这个限制。
额外注意事项
- 确保PostGIS扩展已正确安装,且表的
coordinate列确实是geography(POINT,4326)类型; - 如果你使用
query_as!宏,需要在Cargo.toml中启用sqlx的macros和postgres特性; - 尽量避免使用
unwrap,改用map_err将错误转为sqlx::Error,这样能返回更友好的错误信息。
内容来源于stack exchange

