GeoDjango空间数据迁移总失败?PostGIS扩展与坐标系转换详解(附:实战代码)

编程与开发
Dr.GIS
wowwwai GIS研习社 · 工具流程与项目排障

如果你在做 GeoDjango空间数据迁移总失败?PostGIS扩展与坐标系转换详解(附:实战代码) 这类项目时,经常遇到迁移报错、几何字段无法创建、SRID 不一致、坐标转换后位置偏移等问题,通常不是 Django 本身“坏了”,而是 PostGIS 扩展、数据库权限、空间字段 SRID、GDAL/GEOS 环境或数据坐标系没有配置正确。

引言:GeoDjango空间数据迁移为什么容易失败

GeoDjango 是 Django 内置的 GIS 扩展,常用于把行政区划、道路、地块、POI、遥感解译结果等空间数据接入 Web 系统。它通常搭配 PostgreSQL 和 PostGIS 使用。

在普通 Django 项目中,迁移失败多半是字段类型、默认值或外键约束问题。但在 GeoDjango 项目中,迁移失败还会额外涉及空间数据库能力,例如 PostGIS扩展 是否启用、几何字段是否支持、SRID 是否合法、坐标系转换是否可执行。

本文围绕一个具体问题展开:GeoDjango 模型中定义了空间字段,执行 python manage.py migrate 或导入空间数据时总失败。我们会从数据库扩展、模型字段、坐标系转换、实战代码和排错清单几个角度,把问题拆开处理。

GeoDjango空间数据迁移失败与PostGIS扩展坐标系转换流程图
GeoDjango 空间数据迁移的关键检查点:PostGIS 扩展、空间字段、SRID 与坐标系转换。

背景:常见报错分别指向什么问题

GeoDjango 空间数据迁移失败时,错误信息看起来很多,但可以先归为几类。

报错现象 常见原因 优先检查项
type "geometry" does not exist PostGIS 扩展未启用 数据库中是否执行 CREATE EXTENSION postgis;
function AddGeometryColumn does not exist PostGIS 安装或扩展版本异常 PostGIS 扩展是否安装到当前数据库
Invalid geometry value 导入的 WKT、GeoJSON 或 Shapefile 几何不合法 几何是否为空、自相交、类型不匹配
SRID mismatch 数据 SRID 与模型字段 SRID 不一致 源数据坐标系、模型字段 SRID、数据库字段约束
数据导入成功但地图位置偏移 坐标系声明错误或未真正转换 是否只是设置 SRID,而没有做坐标转换
GDALException GDAL/GEOS/PROJ 环境缺失或路径错误 GeoDjango 运行环境依赖是否可用

对 GIS 读者来说,最需要区分的一点是:设置 SRID 不等于 坐标系转换。很多 GeoDjango 空间数据迁移失败或位置偏移,根源就在这里。

原理:PostGIS扩展、SRID 与坐标系转换的关系

1. PostGIS扩展提供空间字段和空间函数

PostgreSQL 本身是关系型数据库,不原生提供完整的 GIS 空间能力。PostGIS 扩展为 PostgreSQL 增加了 geometrygeography、空间索引、空间关系判断、坐标转换等能力。

GeoDjango 的 PointFieldPolygonFieldMultiPolygonField 等字段,最终会映射为 PostGIS 中的空间字段。如果当前数据库没有启用 PostGIS,迁移时就会出现 type "geometry" does not exist

2. SRID 是坐标参考系统的编号

SRID 是 Spatial Reference Identifier 的缩写,可以理解为空间参考系统编号。常见的例子包括:

  • 4326:WGS 84,经纬度坐标,WebGIS 和 GPS 数据常见。
  • 3857:Web Mercator,互联网地图底图常用。
  • 4490:CGCS2000 地理坐标系,国内数据常见。
  • 45474528 等:CGCS2000 高斯投影分带坐标,常见于工程或自然资源数据。

GeoDjango 模型中的 srid=4326 表示该字段期望存储 SRID 为 4326 的几何对象。PostGIS 可以在字段层面对 SRID 做约束,因此源数据 SRID 不一致时会迁移失败。

3. 设置 SRID 与坐标系转换不是一回事

在 PostGIS 中,ST_SetSRID 只是给几何对象贴上坐标系标签,不改变坐标数值;ST_Transform 才是真正把坐标从一个坐标系转换到另一个坐标系。

操作 是否改变坐标数值 适用场景
ST_SetSRID(geom, 4326) 几何本来就是 4326,只是缺少 SRID 标记
ST_Transform(geom, 4326) 几何实际坐标系不是 4326,需要转换到 4326

如果源数据是投影坐标,例如米制坐标,却被直接 ST_SetSRID 成 4326,经纬度数值会完全不合理,导入后地图自然会偏到错误位置。

步骤:从零检查 GeoDjango 空间数据迁移

步骤一:确认数据库已安装并启用 PostGIS扩展

先连接到目标 PostgreSQL 数据库,而不是只连接到默认的 postgres 数据库。

psql -U postgres -d your_database

检查 PostGIS 是否可用:

SELECT postgis_full_version();

如果提示函数不存在,说明当前数据库还没有启用 PostGIS 扩展。执行:

CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS postgis_topology;

再次检查:

SELECT postgis_full_version();

如果你使用的是普通业务账号连接数据库,而不是超级用户,需要让数据库管理员提前为该数据库创建扩展。很多线上环境的 GeoDjango 空间数据迁移失败,就是因为应用账号没有创建扩展的权限。

步骤二:检查 Django 数据库配置

GeoDjango 连接 PostGIS 时,数据库引擎应使用 GIS 后端,而不是普通 PostgreSQL 后端。

DATABASES = {
    "default": {
        "ENGINE": "django.contrib.gis.db.backends.postgis",
        "NAME": "your_database",
        "USER": "your_user",
        "PASSWORD": "your_password",
        "HOST": "127.0.0.1",
        "PORT": "5432",
    }
}

如果写成下面这种普通后端,空间字段迁移通常会出问题:

"ENGINE": "django.db.backends.postgresql"

还需要确保项目已启用 GIS 应用:

INSTALLED_APPS = [
    "django.contrib.gis",
    "your_app",
]

步骤三:定义正确的 GeoDjango 空间模型

假设我们要存储行政区边界,源数据希望统一入库为 WGS 84,经纬度坐标,也就是 SRID 4326。

from django.contrib.gis.db import models

class AdministrativeArea(models.Model):
    name = models.CharField(max_length=100)
    code = models.CharField(max_length=20, unique=True)
    geom = models.MultiPolygonField(srid=4326)

    class Meta:
        db_table = "administrative_area"

    def __str__(self):
        return self.name

这里有三个容易出错的点:

  • 源数据如果是多个面对象,应使用 MultiPolygonField,不要误用 PolygonField
  • srid=4326 应与最终入库坐标系一致。
  • 如果后续要做空间查询,建议让 GeoDjango 自动为几何字段创建 GiST 空间索引。

然后生成并执行迁移:

python manage.py makemigrations
python manage.py migrate

如果此时仍然报 type "geometry" does not exist,优先回到第一步检查 PostGIS 扩展是否创建在当前业务数据库中。

步骤四:确认源数据的真实坐标系

在导入 Shapefile、GeoJSON、GPKG 等数据前,一定要先确认源数据坐标系。

使用 GDAL 的 ogrinfo 查看:

ogrinfo -so -al data/admin_area.shp

你需要关注输出中的坐标参考系统信息。如果是 Shapefile,还要检查同名的 .prj 文件是否存在。

也可以在 QGIS 中打开图层,右键图层,查看属性中的源坐标参考系统。注意:QGIS 项目坐标系不等于图层真实坐标系,不能只看右下角项目 EPSG 编号。

步骤五:用 GeoDjango LayerMapping 导入并转换坐标系

GeoDjango 提供了 LayerMapping,适合把 Shapefile 等 OGR 支持的数据源导入模型。

示例目录:

your_project/
  manage.py
  your_app/
    models.py
    import_admin_area.py
  data/
    admin_area.shp
    admin_area.shx
    admin_area.dbf
    admin_area.prj

导入脚本示例:

import os
from django.contrib.gis.utils import LayerMapping
from your_app.models import AdministrativeArea

admin_area_mapping = {
    "name": "NAME",
    "code": "CODE",
    "geom": "MULTIPOLYGON",
}

def run(verbose=True):
    shp_path = os.path.abspath("data/admin_area.shp")

    lm = LayerMapping(
        AdministrativeArea,
        shp_path,
        admin_area_mapping,
        transform=True,
        encoding="utf-8",
    )

    lm.save(strict=True, verbose=verbose)

其中 transform=True 表示导入时根据源数据坐标系和模型字段 SRID 做坐标系转换。如果源数据缺少 .prj 或坐标系信息错误,转换就可能失败或结果错误。

在 Django shell 中执行:

python manage.py shell
from your_app.import_admin_area import run
run()

如果中文字段乱码,可以尝试调整 encoding,例如:

encoding="gbk"

步骤六:用 PostGIS SQL 修复已入库的 SRID 问题

如果数据已经导入,但 SRID 错误,可以先检查:

SELECT id, ST_SRID(geom) FROM administrative_area LIMIT 10;

如果几何坐标本身就是 WGS 84,只是 SRID 显示为 0,可以使用:

UPDATE administrative_area
SET geom = ST_SetSRID(geom, 4326)
WHERE ST_SRID(geom) = 0;

如果几何实际是 EPSG:3857,需要转换到 4326,应使用:

UPDATE administrative_area
SET geom = ST_Transform(ST_SetSRID(geom, 3857), 4326)
WHERE ST_SRID(geom) = 0;

如果字段已有错误 SRID,例如被错误标记为 4326,但实际坐标是 3857,需要谨慎处理:

UPDATE administrative_area
SET geom = ST_Transform(ST_SetSRID(geom, 3857), 4326);

这类操作前必须备份数据,因为一旦你把“真实坐标系”判断错了,转换结果会继续错误。

步骤七:验证迁移结果是否正确

不要只看迁移命令是否成功,还要验证空间数据是否真的可用。

检查记录数:

SELECT COUNT(*) FROM administrative_area;

检查 SRID:

SELECT DISTINCT ST_SRID(geom) FROM administrative_area;

检查边界范围:

SELECT ST_Extent(geom) FROM administrative_area;

如果是中国范围的 4326 经纬度数据,范围大致应落在合理经纬度区间内。例如经度通常在 70 到 140 左右,纬度通常在 15 到 55 左右。若出现几百万级别坐标值,说明很可能还是投影坐标,没有转换成经纬度。

也可以导出 GeoJSON 快速查看:

SELECT json_build_object(
    'type', 'FeatureCollection',
    'features', json_agg(
        json_build_object(
            'type', 'Feature',
            'properties', json_build_object('name', name, 'code', code),
            'geometry', ST_AsGeoJSON(geom)::json
        )
    )
)
FROM administrative_area;

常见坑:GeoDjango空间数据迁移失败的高频原因

1. PostGIS扩展创建在了错误数据库

很多人用 psql 登录后直接执行 CREATE EXTENSION postgis;,但实际连接的是默认数据库,而 Django 配置连接的是另一个业务数据库。结果就是命令看似执行了,迁移仍然报错。

解决方法是明确指定数据库:

psql -U postgres -d your_database

2. 模型字段类型与源数据几何类型不一致

如果源数据中有多个面组成的要素,但模型写成 PolygonField,导入时可能失败。行政区、宗地、生态红线等数据经常是 MultiPolygon,建议先用工具检查几何类型。

ogrinfo -so -al data/admin_area.shp

如果输出中是 Multi Polygon,模型应使用:

geom = models.MultiPolygonField(srid=4326)

3. 只设置 SRID,没有真正做坐标系转换

这是 GeoDjango 坐标系转换中最容易踩的坑。源数据是米制投影坐标,却直接把 SRID 设置为 4326,会导致地图位置严重偏移。

正确思路是:先确认源坐标系,再使用 ST_TransformLayerMapping(transform=True) 做转换。

4. 源数据缺少 .prj 文件

Shapefile 的坐标系通常写在 .prj 文件中。如果缺少该文件,GeoDjango 很难自动判断源坐标系。此时需要向数据提供方确认坐标系,或在 QGIS 中正确指定图层 CRS 后另存为带坐标系的新文件。

5. GDAL、GEOS、PROJ 环境没有配置好

GeoDjango 依赖 GDAL、GEOS、PROJ 等底层 GIS 库。开发环境中如果这些库缺失,可能会在启动项目、导入数据或坐标转换时报错。

在服务器部署时,建议先用最小命令验证依赖可用:

python manage.py shell
from django.contrib.gis.geos import Point
p = Point(116.39, 39.90, srid=4326)
print(p)

如果这一步都失败,应先修复 GeoDjango 环境,而不是继续排查模型代码。

方法比较:GeoDjango、PostGIS SQL 与 GDAL 哪种方式更适合迁移

方法 适用场景 优点 限制
GeoDjango Migration 创建空间表结构、维护模型版本 与 Django 项目一致,便于团队协作 不负责清洗复杂空间数据
GeoDjango LayerMapping 中小规模 Shapefile 导入模型 代码清晰,能与模型字段映射 依赖源数据坐标系信息,超大数据效率一般
PostGIS SQL 批量修复 SRID、转换坐标系、建索引 直接、高效、适合数据库内处理 需要熟悉 SQL 和 PostGIS 函数
ogr2ogr 大批量格式转换和入库 稳定,支持格式多,适合自动化脚本 字段映射和业务校验不如 Django 模型直观
QGIS 手工处理 一次性检查、可视化验证、修复小数据 直观,适合教学和排错 不适合长期自动化生产流程

实际项目中,推荐组合使用:用 Django Migration 管理表结构,用 LayerMapping 或 ogr2ogr 导入数据,用 PostGIS SQL 做坐标系和质量检查,用 QGIS 做可视化验证。

检查清单:迁移前后逐项确认

迁移前检查

  • PostgreSQL 当前业务数据库是否启用了 postgis 扩展。
  • Django 数据库引擎是否为 django.contrib.gis.db.backends.postgis
  • INSTALLED_APPS 中是否包含 django.contrib.gis
  • 模型空间字段类型是否与源数据几何类型一致。
  • 模型字段 SRID 是否与目标入库坐标系一致。
  • 源数据是否有明确 CRS,例如 Shapefile 是否有 .prj
  • GDAL、GEOS、PROJ 是否在运行环境中可用。

导入时检查

  • 如果用 LayerMapping,是否设置了正确的字段映射。
  • 如果需要坐标转换,是否启用了 transform=True
  • 中文属性字段是否设置了正确编码,例如 utf-8gbk
  • 导入失败时是否先定位到具体要素,而不是直接忽略全部错误。

迁移后检查

  • 记录数是否符合预期。
  • ST_SRID 是否统一且正确。
  • ST_Extent 输出范围是否符合地理常识。
  • 空间索引是否创建。
  • 在 QGIS、Leaflet 或 OpenLayers 中叠加底图后位置是否正确。

FAQ:GeoDjango空间数据迁移常见问题

Q1:为什么已经安装 PostGIS,GeoDjango 迁移还是提示 geometry 不存在?

因为 PostGIS 需要在具体数据库中启用,不是安装一次就对所有数据库自动生效。请连接 Django 实际使用的数据库,执行 CREATE EXTENSION IF NOT EXISTS postgis;,再重新迁移。

Q2:GeoDjango 模型中的 srid=4326 会自动把数据转换成 4326 吗?

不会。模型字段的 srid=4326 表示该字段期望存储 4326 的几何。导入数据时是否转换,取决于你的导入方法。例如 LayerMapping(transform=True) 可以尝试转换,但前提是源数据坐标系信息正确。

Q3:ST_SetSRID 和 ST_Transform 应该用哪个?

如果几何坐标本来就是目标坐标系,只是缺少 SRID 标签,用 ST_SetSRID。如果几何坐标需要从一个坐标系变到另一个坐标系,用 ST_Transform。两者不能混用。

Q4:导入 Shapefile 时中文字段乱码怎么办?

先确认源数据编码。国内历史 Shapefile 常见 GBK 编码,新数据可能是 UTF-8。使用 LayerMapping 时可以设置 encoding="gbk"encoding="utf-8"。如果仍然乱码,可以先用 QGIS 或 ogr2ogr 转换为 UTF-8。

Q5:GeoDjango 空间数据迁移成功后,还需要手动创建空间索引吗?

通常 GeoDjango 会为空间字段创建 GiST 索引,但建议在数据库中确认。空间查询慢时,可以检查索引是否存在,并使用 EXPLAIN 查看查询计划。

SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'administrative_area';

Q6:源数据是 CGCS2000,能直接当成 WGS 84 使用吗?

不能简单一概而论。CGCS2000 与 WGS 84 在很多 Web 展示场景下差异较小,但在测绘、工程、自然资源等对精度敏感的业务中,仍应严格按坐标参考系统处理。尤其是 CGCS2000 投影坐标,必须转换后才能作为 4326 经纬度使用。

结论:先修 PostGIS,再理清坐标系,最后验证空间结果

GeoDjango空间数据迁移失败并不可怕,关键是不要只盯着 Django 报错。完整排查顺序应该是:先确认 PostGIS 扩展启用在正确数据库中,再检查 Django GIS 后端和模型空间字段,然后确认源数据真实坐标系,最后用 LayerMappingST_Transform 或 ogr2ogr 完成坐标系转换。

如果你记住一个原则,就是:SRID 是空间数据的身份信息,坐标系转换是坐标值的数学变换。把这两件事分清楚,大多数 GeoDjango 空间数据迁移、PostGIS扩展和坐标系转换问题都能快速定位并修复。