惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
Netflix TechBlog - Medium
Microsoft Azure Blog
Microsoft Azure Blog
罗磊的独立博客
博客园 - 三生石上(FineUI控件)
aimingoo的专栏
aimingoo的专栏
B
Blog RSS Feed
V
Visual Studio Blog
P
Proofpoint News Feed
云风的 BLOG
云风的 BLOG
博客园 - 【当耐特】
大猫的无限游戏
大猫的无限游戏
Application and Cybersecurity Blog
Application and Cybersecurity Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
The Cloudflare Blog
B
Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Apple Machine Learning Research
Apple Machine Learning Research
M
MIT News - Artificial intelligence
Know Your Adversary
Know Your Adversary
I
InfoQ
T
The Exploit Database - CXSecurity.com
V
Vulnerabilities – Threatpost
C
Cisco Blogs
Spread Privacy
Spread Privacy
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
P
Palo Alto Networks Blog
Simon Willison's Weblog
Simon Willison's Weblog
月光博客
月光博客
博客园 - Franky
Project Zero
Project Zero
G
Google Developers Blog
S
SegmentFault 最新的问题
博客园 - 聂微东
P
Privacy & Cybersecurity Law Blog
The GitHub Blog
The GitHub Blog
阮一峰的网络日志
阮一峰的网络日志
P
Privacy International News Feed
T
Threat Research - Cisco Blogs
S
Schneier on Security
Microsoft Security Blog
Microsoft Security Blog
G
GRAHAM CLULEY
S
Security @ Cisco Blogs
Martin Fowler
Martin Fowler
A
Arctic Wolf
T
Tenable Blog
L
LINUX DO - 最新话题
TaoSecurity Blog
TaoSecurity Blog
Hugging Face - Blog
Hugging Face - Blog
有赞技术团队
有赞技术团队
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com

博客园 - 我才是银古

第16章:常见问题、排错与最佳实践 第15章:扩展生态、MCAD 与外部集成 第12章:实战案例:机械结构与 3D 打印零件 第14章:构建、测试、调试与贡献流程 第13章:OpenSCAD 源码架构与核心执行流程 第11章:预览、渲染、网格精度与性能优化 第09章:列表推导、递归与算法建模 第08章:参数化零件库与复用设计 第10章:导入导出、命令行与自动化 第06章:CSG 布尔建模方法 第07章:二维图形、拉伸、旋转与投影 第05章:基础几何、坐标系与变换 第04章:参数、变量、函数、模块与作用域 OpenSCAD 教程目录 第03章:OpenSCAD 语言基础 第02章:安装、环境配置与开发工作流 第01章:OpenSCAD 项目全景与学习路线 第02章:源码获取、编译与开发环境配置 第01章:OCCT项目全景与学习路线 第18章:二次开发实战与综合案例 第18章:综合实战案例 第17章:数据交换与协同 第16章:源码架构与二次开发 第15章:插件与自定义工作台开发 第14章:Python脚本宏与自动化 第13章:FEM仿真分析 第12章:CAM数控加工 第11章:SurfaceMesh与逆向工程 第10章:Draft二维绘图与BIM建筑 第09章:工程图TechDraw 第07章:参数化表达式与Spreadsheet 第08章:装配设计Assembly 第06章:Part工作台与几何内核 第05章:PartDesign实体特征建模 第04章:草图Sketcher约束建模 第02章:安装版本与工作环境配置 第03章:界面工作台与基础操作 第01章:项目全景与学习路线 第十二章:插件开发、研究功能与最佳实践 第十章:定时任务与自动化(Cron) 第七章:技能、记忆与自学习闭环 第八章:MCP 集成与上下文文件 第六章:工具系统与终端后端 第五章:模型供应商与配置体系 Hermes Agent 教程目录 第十一章:语音、视觉、浏览器与子代理协作 第四章:CLI/TUI 与会话管理 第十二章:学习路线、实战方案与最佳实践 第十一章:源码结构、开发调试与插件开发 第十章:自动化、远程访问、日志与排障 第九章:Control UI、节点、Canvas 与语音能力 第七章:工具、技能、插件与能力扩展 第八章:安全模型、访问控制与沙箱实践 第六章:Agent 工作区、会话与多智能体路由 第五章:多通道消息接入与聊天平台配置 第四章:配置体系、模型接入与认证管理 第三章:Gateway 架构、协议与运行机制 第二章:安装、环境准备与快速上手 第一章:OpenClaw 项目概览与核心定位 oh-my-openagent 教程目录 09-命令模型回退与配置参考 10-实战案例最佳实践与故障排除 05-工作模式-Ultrawork-Prometheus-Atlas 08-Hooks与MCP系统 06-Category与Skill系统 07-核心工具链 04-智能体全景详解 03-安装与环境配置 02-整体架构与多模型编排机制 01-项目简介与核心理念 01-项目概览与学习路线 02-安装部署与工具适配 03-Skill机制与using-superpowers 05-TDD系统化调试与完成前验证 04-需求澄清方案设计与计划编写 07-并行智能体子智能体与Git-Worktree 第六章:代码审查、反馈处理与分支收尾 08-中国特色Skills与本土团队落地 09-MCP构建工作流执行与自定义Skill 第23章:FreeCAD-Python-API Clipper2 C# 源码解读教程 第19章:PolyTree 多边形树结构 第20章:实际应用与最佳实践 第18章:Minkowski 和与差 第17章:RectClip 矩形裁剪优化 第16章:ClipperOffset 偏移类详解 第15章:填充规则详解 第14章:布尔运算执行流程 第13章:ClipperD 浮点裁剪类 第11章:OutRec 与 OutPt 输出结构 第9章:Active 活动边结构 第10章:Vertex 顶点与 LocalMinima 局部极小值 第12章:Clipper64 裁剪类详解 第7章:高精度运算与128位整数 第8章:ClipperBase 基类详解 第5章:枚举类型与常量定义 第6章:InternalClipper 内部工具类 第2章:核心数据结构 - Point64、PointD 第3章:路径与多边形表示 - Path64、PathD、Paths64、PathsD 第4章:矩形边界 - Rect64、RectD
PostGIS数据库
我才是银古 · 2026-06-21 · via 博客园 - 我才是银古

PostGIS数据库

概述

PostGIS是PostgreSQL数据库的空间扩展,提供了强大的空间数据存储、查询和分析功能,是企业级GIS应用的首选数据库方案。

数据库连接

连接参数模型

/**
 * 数据库连接参数模型
 */
@Data
public class DbConnBaseModel implements Serializable {
    private String dbtype = "postgis";
    private String host;
    private Integer port;
    private String schema;
    private String database;
    private String user;
    private String passwd;
}

GeoTools连接方式

/**
 * 获取Postgis数据源参数
 */
private static Map<String, Object> getPostgisInfo(DbConnBaseModel dbConnBaseModel) {
    Map<String, Object> params = new HashMap<>();
    params.put("dbtype", dbConnBaseModel.getDbtype());
    params.put("host", dbConnBaseModel.getHost());
    params.put("port", dbConnBaseModel.getPort());
    params.put("schema", dbConnBaseModel.getSchema());
    params.put("database", dbConnBaseModel.getDatabase());
    params.put("user", dbConnBaseModel.getUser());
    params.put("passwd", dbConnBaseModel.getPasswd());
    params.put("preparedStatements", true);
    params.put("encode functions", true);
    return params;
}

/**
 * 获取Postgis数据源
 */
public static JDBCDataStore getPostgisDataStore(DbConnBaseModel dbConnBaseModel) {
    Map<String, Object> params = getPostgisInfo(dbConnBaseModel);
    return (JDBCDataStore) DataStoreFinder.getDataStore(params);
}

GDAL连接方式

/**
 * 获取GDAL Postgis连接字符串
 */
public static String toGdalPostgisConnStr(DbConnBaseModel dbConnBaseModel) {
    return "PG: host=" + dbConnBaseModel.getHost() +
        " port=" + dbConnBaseModel.getPort() +
        " dbname=" + dbConnBaseModel.getDatabase() +
        " user=" + dbConnBaseModel.getUser() +
        " password=" + dbConnBaseModel.getPasswd() +
        " active_schema=" + dbConnBaseModel.getSchema();
}

数据读取

使用GeoTools读取

/**
 * Postgis图层转换为WktLayer
 */
public static WktLayer fromPostGIS(DbConnBaseModel dbConnBaseModel, 
        String layerName, String attributeFilter, String spatialFilterWkt, 
        GisEngineType gisEngineType) {
    
    gisEngineType = GisEngineType.getGisEngineType(gisEngineType);
    
    if (gisEngineType == GisEngineType.GEOTOOLS) {
        DataStore dataStore = PostgisUtil.getPostgisDataStore(dbConnBaseModel);
        SimpleFeatureSource source = dataStore.getFeatureSource(layerName);
        
        // 应用过滤条件
        SimpleFeatureCollection simpleFeatureCollection = 
            GeotoolsUtil.filter(source, attributeFilter, spatialFilterWkt);
        
        dataStore.dispose();
        return fromSimpleFeatureCollection(simpleFeatureCollection);
    }
    
    return null;
}

使用GDAL读取

/**
 * 使用GDAL读取PostGIS
 */
public static WktLayer fromPostGISWithGDAL(DbConnBaseModel dbConnBaseModel,
        String layerName, String attributeFilter, String spatialFilterWkt) {
    String connStr = PostgisUtil.toGdalPostgisConnStr(dbConnBaseModel);
    return OgrUtil.layer2WktLayer(DataFormatType.POSTGIS, connStr, 
        layerName, attributeFilter, spatialFilterWkt);
}

数据写入

使用GeoTools写入

/**
 * WktLayer转换为Postgis图层
 */
public static void toPostGIS(WktLayer wktLayer, DbConnBaseModel dbConnBaseModel, 
        String layerName, GisEngineType gisEngineType) {
    
    wktLayer.check();
    EsriUtil.excludeSpecialFields(wktLayer.getFields());
    
    gisEngineType = GisEngineType.getGisEngineType(gisEngineType);
    
    if (gisEngineType == GisEngineType.GEOTOOLS) {
        SimpleFeatureCollection featureCollection = toSimpleFeatureCollection(wktLayer);
        SimpleFeatureType simpleFeatureType = featureCollection.getSchema();
        
        JDBCDataStore dataStore = PostgisUtil.getPostgisDataStore(dbConnBaseModel);
        
        // 检查表是否存在,不存在则创建
        if (!Arrays.asList(dataStore.getTypeNames()).contains(layerName)) {
            SimpleFeatureTypeBuilder tb = new SimpleFeatureTypeBuilder();
            tb.init(simpleFeatureType);
            tb.setName(layerName);
            dataStore.createSchema(tb.buildFeatureType());
        }
        
        // 构建字段映射
        SimpleFeatureType featureType = dataStore.getSchema(layerName);
        Map<String, String> fieldMap = buildFieldMapping(featureType, simpleFeatureType);
        
        dataStore.dispose();
        
        // 批量写入
        batchWrite(dbConnBaseModel, layerName, featureCollection, fieldMap);
    }
}

/**
 * 批量写入数据
 */
private static void batchWrite(DbConnBaseModel dbConnBaseModel, String layerName,
        SimpleFeatureCollection featureCollection, Map<String, String> fieldMap) {
    
    List<SimpleFeature> features = new ArrayList<>();
    try (FeatureIterator<SimpleFeature> iterator = featureCollection.features()) {
        while (iterator.hasNext()) {
            features.add(iterator.next());
        }
    }
    
    int batchSize = 1000;
    int count = features.size() / batchSize;
    ExecutorService executorService = ThreadUtil.newExecutor(count);
    
    for (int i = 0; i <= count; i++) {
        List<SimpleFeature> subList;
        if (i == count) {
            subList = features.subList(i * batchSize, features.size());
        } else {
            subList = features.subList(i * batchSize, (i + 1) * batchSize);
        }
        
        executorService.execute(() -> {
            writeBatch(dbConnBaseModel, layerName, subList, fieldMap);
        });
    }
    
    executorService.shutdown();
    executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
}

/**
 * 写入单批数据
 */
private static void writeBatch(DbConnBaseModel dbConnBaseModel, String layerName,
        List<SimpleFeature> subList, Map<String, String> fieldMap) {
    try {
        DataStore ds = PostgisUtil.getPostgisDataStore(dbConnBaseModel);
        Transaction transaction = new DefaultTransaction("create");
        FeatureWriter<SimpleFeatureType, SimpleFeature> writer = 
            ds.getFeatureWriterAppend(layerName, transaction);
        
        try {
            for (SimpleFeature feature : subList) {
                writer.hasNext();
                SimpleFeature writefeature = writer.next();
                writefeature.setDefaultGeometry(feature.getDefaultGeometry());
                
                for (Map.Entry<String, String> kv : fieldMap.entrySet()) {
                    writefeature.setAttribute(kv.getKey(), 
                        feature.getAttribute(kv.getValue()));
                }
                
                writer.write();
            }
            
            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
            throw new RuntimeException(e);
        } finally {
            writer.close();
            transaction.close();
            ds.dispose();
        }
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

使用GDAL写入

/**
 * 使用GDAL写入PostGIS
 */
public static void toPostGISWithGDAL(WktLayer wktLayer, 
        DbConnBaseModel dbConnBaseModel, String layerName) {
    Vector options = new Vector();
    options.add("GEOMETRY_NAME=SHAPE");
    options.add("FID=FID");
    options.add("FID64=TRUE");
    
    String path = PostgisUtil.toGdalPostgisConnStr(dbConnBaseModel);
    OgrUtil.wktLayer2Layer4Postgis(DataFormatType.POSTGIS, dbConnBaseModel, 
        wktLayer, layerName, options);
}

GDAL批量写入优化

/**
 * GDAL批量写入PostGIS(优化版)
 */
private static void wktLayer2Layer4Postgis(DataFormatType driverType, String path, 
        WktLayer wktLayer, String layerName) {
    int batchSize = 1000;
    int count = wktLayer.getFeatures().size() / batchSize;
    ExecutorService executorService = ThreadUtil.newExecutor(count);
    
    for (int j = 0; j <= count; j++) {
        List<WktFeature> subList;
        if (j == count) {
            subList = wktLayer.getFeatures().subList(j * batchSize, 
                wktLayer.getFeatures().size());
        } else {
            subList = wktLayer.getFeatures().subList(j * batchSize, 
                (j + 1) * batchSize);
        }
        
        executorService.execute(() -> {
            try {
                wktFeatures2Layer(driverType, path, wktLayer.getFields(), 
                    subList, layerName);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });
    }
    
    executorService.shutdown();
    executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
}

数据删除

/**
 * 删除PostGIS指定图层的指定要素
 */
public static int deletePostgisFeatures(DbConnBaseModel dbConnBaseModel, 
        String layerName, String whereClause) {
    JDBCDataStore dataStore = PostgisUtil.getPostgisDataStore(dbConnBaseModel);
    Statement statement = dataStore.getConnection(Transaction.AUTO_COMMIT)
        .createStatement();
    
    String sql = String.format("DELETE FROM %s.%s", 
        dbConnBaseModel.getSchema(), layerName);
    if (CharSequenceUtil.isNotBlank(whereClause)) {
        sql += " WHERE " + whereClause;
    }
    
    int count = statement.executeUpdate(sql);
    
    statement.close();
    dataStore.dispose();
    return count;
}

实践案例

案例1:Shapefile导入PostGIS

/**
 * 将Shapefile导入PostGIS
 */
public void importShpToPostGIS(String shpPath, DbConnBaseModel dbConn, 
        String tableName) {
    // 读取Shapefile
    WktLayer layer = WktLayerConverter.fromShapefile(
        shpPath, null, null, GisEngineType.GEOTOOLS);
    
    // 写入PostGIS
    WktLayerConverter.toPostGIS(layer, dbConn, tableName, GisEngineType.GEOTOOLS);
    
    System.out.println("导入完成,共 " + layer.getFeatures().size() + " 条记录");
}

案例2:PostGIS导出Shapefile

/**
 * 从PostGIS导出Shapefile
 */
public void exportPostGISToShp(DbConnBaseModel dbConn, String tableName,
        String outputPath, String whereClause) {
    // 读取PostGIS
    WktLayer layer = WktLayerConverter.fromPostGIS(
        dbConn, tableName, whereClause, null, GisEngineType.GEOTOOLS);
    
    // 写入Shapefile
    WktLayerConverter.toShapefile(layer, outputPath, GisEngineType.GEOTOOLS);
    
    System.out.println("导出完成,共 " + layer.getFeatures().size() + " 条记录");
}

案例3:空间查询

/**
 * 空间查询:找出指定区域内的要素
 */
public List<WktFeature> spatialQueryFromPostGIS(DbConnBaseModel dbConn,
        String tableName, String boundaryWkt) {
    // 使用空间过滤读取
    WktLayer layer = WktLayerConverter.fromPostGIS(
        dbConn, tableName, null, boundaryWkt, GisEngineType.GEOTOOLS);
    
    return layer.getFeatures();
}

案例4:增量更新

/**
 * 增量更新PostGIS数据
 */
public void incrementalUpdate(DbConnBaseModel dbConn, String tableName,
        WktLayer newData, String keyField) {
    // 获取新数据的Key列表
    Set<String> newKeys = newData.getFeatures().stream()
        .map(f -> {
            Optional<WktFieldValue> opt = f.getFieldValues().stream()
                .filter(v -> v.getField().getYwName().equals(keyField))
                .findFirst();
            return opt.map(v -> v.getValue().toString()).orElse("");
        })
        .collect(Collectors.toSet());
    
    // 删除旧数据
    String whereClause = keyField + " IN ('" + String.join("','", newKeys) + "')";
    int deleted = PostgisUtil.deletePostgisFeatures(dbConn, tableName, whereClause);
    System.out.println("删除旧数据 " + deleted + " 条");
    
    // 写入新数据
    WktLayerConverter.toPostGIS(newData, dbConn, tableName, GisEngineType.GEOTOOLS);
    System.out.println("写入新数据 " + newData.getFeatures().size() + " 条");
}

案例5:数据同步

/**
 * 同步两个PostGIS表的数据
 */
public void syncPostGISTables(DbConnBaseModel sourceDb, String sourceTable,
        DbConnBaseModel targetDb, String targetTable, String whereClause) {
    // 读取源数据
    WktLayer layer = WktLayerConverter.fromPostGIS(
        sourceDb, sourceTable, whereClause, null, GisEngineType.GEOTOOLS);
    
    // 清空目标表
    PostgisUtil.deletePostgisFeatures(targetDb, targetTable, null);
    
    // 写入目标表
    WktLayerConverter.toPostGIS(layer, targetDb, targetTable, GisEngineType.GEOTOOLS);
    
    System.out.println("同步完成,共 " + layer.getFeatures().size() + " 条记录");
}

案例6:坐标转换后入库

/**
 * 坐标转换后写入PostGIS
 */
public void transformAndImport(String shpPath, DbConnBaseModel dbConn,
        String tableName, int targetWkid) {
    // 读取Shapefile
    WktLayer layer = WktLayerConverter.fromShapefile(
        shpPath, null, null, GisEngineType.GEOTOOLS);
    
    // 坐标转换
    if (layer.getWkid() != targetWkid) {
        layer = CrsUtil.reproject(layer, targetWkid);
    }
    
    // 写入PostGIS
    WktLayerConverter.toPostGIS(layer, dbConn, tableName, GisEngineType.GEOTOOLS);
}

PostGIS SQL示例

虽然本教程主要使用Java API,但了解PostGIS的SQL语法也很有帮助:

空间查询

-- 点在面内查询
SELECT * FROM parcels 
WHERE ST_Contains(shape, ST_GeomFromText('POINT(116.397 39.908)', 4490));

-- 缓冲区查询
SELECT * FROM points 
WHERE ST_DWithin(shape, ST_GeomFromText('POINT(116.397 39.908)', 4490), 0.01);

-- 相交查询
SELECT * FROM buildings 
WHERE ST_Intersects(shape, ST_GeomFromText('POLYGON(...)', 4490));

空间计算

-- 计算面积
SELECT id, ST_Area(shape) as area FROM parcels;

-- 计算缓冲区
SELECT id, ST_Buffer(shape, 100) as buffer FROM roads;

-- 计算交集
SELECT a.id, ST_Intersection(a.shape, b.shape) as intersection
FROM layer_a a, layer_b b
WHERE ST_Intersects(a.shape, b.shape);

坐标转换

-- 坐标转换
SELECT id, ST_Transform(shape, 4526) as shape_proj FROM parcels;

性能优化

1. 空间索引

-- 创建空间索引
CREATE INDEX idx_parcels_shape ON parcels USING GIST(shape);

2. 批量写入

使用多线程批量写入,每批1000条左右:

int batchSize = 1000;
ExecutorService executor = ThreadUtil.newExecutor(Runtime.getRuntime().availableProcessors());

3. 使用PreparedStatement

GeoTools连接参数中启用:

params.put("preparedStatements", true);

4. 合理使用过滤

优先使用数据库端过滤,减少数据传输量:

// 推荐:使用属性过滤
WktLayerConverter.fromPostGIS(dbConn, tableName, "TYPE='residential'", null, ...);

// 推荐:使用空间过滤
WktLayerConverter.fromPostGIS(dbConn, tableName, null, boundaryWkt, ...);

小结

本章介绍了PostGIS数据库的核心内容:

  1. 数据库连接:GeoTools和GDAL两种连接方式
  2. 数据读取:支持属性过滤和空间过滤
  3. 数据写入:批量写入提升性能
  4. 数据删除:按条件删除要素
  5. 性能优化:空间索引、批量操作、数据库端过滤

下一章将介绍国土TXT格式的处理方法。