이번에 회사에서 기존 솔루션의 기능을 고도화하는 작업에 착수했습니다.
고도화의 내용은 ShapeFile
을 PostGIS Table
에 import
하는 작업의
속도 개선입니다. 그리고 해당 기능에 Geotools
를 사용해는 것을 추천해주셔서
한번 해보기로 했습니다.
하지만 아직 GeoTools
를 통한 Shapefile
, PostGIS
를 제어해본 적이
없다보니, 제 PC 에서 먼저 테스트 코드를 작성해보기로 했습니다.
이 글은 바로 해당 테스트 코드에 대한 기록이며,
추가적으로 제가 생각하는 GeoTools
의 불편한 점들을 작성해봤습니다.
GeoTools
를 공부하시는 분들에게 부디 도움이 되길 바랍니다.
저는 Spring Boot 기반 환경에서 작업하는 걸 좋아해서
pom.xml 에 maven 과 관련된 내용이 약간 섞여 있습니다.
Geotools 관련된 것들은 명시적으로 표기해서 구별이 가능하도록 했습니다.
GeoTools 세팅
라고 표기된 것만 확인하면 됩니다.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.10</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>coding.toast</groupId>
<artifactId>geotools</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>geotools-playground</name>
<description>geotools-playground</description>
<properties>
<java.version>17</java.version>
<!-- GeoTools 세팅(1) - geotools 버전 통일을 위한 프로퍼티 설정 -->
<geotools.version>27.2</geotools.version>
</properties>
<!-- GeoTools 세팅(2) - repository 추가 [시작] -->
<repositories>
<repository>
<id>osgeo</id>
<name>Geotools repository</name>
<url>https://repo.osgeo.org/repository/release</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<snapshots>
<enabled>false</enabled>
</snapshots>
<id>osgeo-snapshot</id>
<name>Central Repository</name>
<url>https://repo.osgeo.org/repository/snapshot</url>
</repository>
</repositories>
<!-- GeoTools 세팅(2) - repository 추가 [끝] -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- GeoTools 세팅(3) : 의존성 추가 [시작] -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.geotools.jdbc</groupId>
<artifactId>gt-jdbc-postgis</artifactId>
<version>${geotools.version}</version>
</dependency>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-shapefile</artifactId>
<version>${geotools.version}</version>
</dependency>
<!--https://docs.geotools.org/latest/userguide/library/referencing/index.html-->
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-epsg-hsql</artifactId>
<version>${geotools.version}</version>
</dependency>
<!-- gt-epsg-hsql 는 gt-referencing API 의 구현체입니다. -->
<!--
참고로 gt-referencing API 구현체는 gf-referencinig 외에도
gt-epsg-wkt 도 있습니다만 사용하지 않는 걸 추천합니다.
gt-epsg-wkt 는 prj 파일에 기재된 WKT 문자열을 제대로 번역하지 못하는
경우가 많기 때문입니다. 예를 들어서 5186 좌표계를 사용하는 prj 파일이
ESRI WKT 로 작성되어 있다면 EPSG 코드를 추출하지 못합니다.
-->
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-referencing</artifactId>
<version>${geotools.version}</version>
</dependency>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-metadata</artifactId>
<version>${geotools.version}</version>
</dependency>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-opengis</artifactId>
<version>${geotools.version}</version>
</dependency>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-wfs-ng</artifactId>
<version>${geotools.version}</version>
</dependency>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-process</artifactId>
<version>${geotools.version}</version>
</dependency>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-transform</artifactId>
<version>${geotools.version}</version>
</dependency>
<dependency>
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
<version>1.19.0</version>
</dependency>
<!-- GeoTools 세팅(3) : 의존성 추가 [끝] -->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
참고
여기 작성된 테스트 코드는 모두 저의 github repository 에 기재될 겁니다.
repository 주소: https://github.com/CodingToastBread/geotools-playground
package coding.toast.geotools.shapefile;
import coding.toast.geotools.utils.ShapeFileUtil;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.feature.type.GeometryDescriptorImpl;
import org.geotools.referencing.CRS;
import org.junit.jupiter.api.Test;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
public class ShapeFileMetaDataReadTests {
@Test
void readShapeFileMetaData() throws IOException, FactoryException {
ShapefileDataStore shapeFileDataStore
= ShapeFileUtil.getShapeFileDataStore(
"C:/shapefiles/sample/sample.shp",
"UTF-8");
CoordinateReferenceSystem shapeFileCrs
= shapeFileDataStore.getSchema().getCoordinateReferenceSystem();
// (1) EPSG CODE 코드 추출
// null 이 나올 수도 있습니다. 이건 gt-reference 구현체 라이브러리가
// 제대로 CRS 를 읽지 못하거나, 정말 shapefile prj 가 이상한 겁니다.
Integer epsgCode = CRS.lookupEpsgCode(shapeFileCrs, false);
System.out.println("\n(1) EPSG CODE : " + epsgCode);
// (2) TypeName 추출
// GeoTools 가 인식하는 shapefile 의 entry(=type) 명을 뽑아낸다.
// 이 type 명칭은 추후에 datastore 에 있는 다양한 entry 에 대해서
// 하나만 뽑아낼 때 사용하게 되는 중요한 값이다.
String typeName = shapeFileDataStore.getTypeNames()[0]; // 쉐이프 파일을 하나 지정했으므로 무조건 TypeNames 가 1개이다.
System.out.println("\n(2) TypeName 추출 : " + Arrays.toString(shapeFileDataStore.getTypeNames()));
// (3) 스키마 정보 조회
// SimpleFeatureType schema = shapeFileDataStore.getSchema();
SimpleFeatureType schema = shapeFileDataStore.getSchema(typeName);
System.out.println("\n스키마 정보 조회 =: " + schema);
// (4) geometry attribute 명칭 추출
// 참고: 사실 dbf 파일에는 geometry 를 위한 attribute 가 존재하지 않습니다.
// 하지만 GeoTools 는 편의를 위해서 임의로 the_geom 이라는 이름으로 하나의 attribute 를 제공합니다.
String geomName = schema.getGeometryDescriptor().getLocalName();
System.out.println("\n(4) geometry attribute 명칭 추출 : " + geomName);
// (5) geometry type 추출하기
String geomTypeName = schema.getGeometryDescriptor().getType().getBinding().getSimpleName();
System.out.println("\n(5) geometry type 추출하기 : " + geomTypeName);
// (6) Feature 각각 속성 정보 조회.
System.out.println("\n============================= (6) Feature 각각 속성 정보 조회 [START] =============================");
List<AttributeDescriptor> attributeDescriptors = schema.getAttributeDescriptors();
for (AttributeDescriptor attributeDescriptor : attributeDescriptors) {
// 지오메트리 타입에 대한 어떤 특별한 조회를 원하면?
if (attributeDescriptor instanceof GeometryDescriptorImpl) {
System.out.println("\nGeometry 속성 발견!");
GeometryDescriptorImpl geometryDescriptor = (GeometryDescriptorImpl) attributeDescriptor;
System.out.println("Geometry Attr LocalName" + geometryDescriptor.getLocalName());
System.out.println("Geometry Attr CoordinateReferenceSystem" + geometryDescriptor.getCoordinateReferenceSystem());
System.out.println("Geometry Attr Type" + geometryDescriptor.getType());
continue;
}
// 나머지 AttributeTypeImpl 타입에 대한 조회
System.out.println("Attribute name : " + attributeDescriptor.getName()
+ " , Attribute Type : " + attributeDescriptor.getType().getClass()
+ " , Attribute Binding Data Type : "
+ attributeDescriptor.getType().getBinding().getSimpleName());
}
System.out.println("\n============================= (6) Feature 각각 속성 정보 조회 [END] =============================");
}
}
출력 예시
:
package coding.toast.geotools.shapefile;
import coding.toast.geotools.utils.DataStoreUtil;
import coding.toast.geotools.utils.OpenEpsgMapUtil;
import coding.toast.geotools.utils.ShapeFileUtil;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.data.store.ContentFeatureCollection;
import org.geotools.data.store.ContentFeatureSource;
import org.junit.jupiter.api.Test;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.type.AttributeDescriptor;
import java.io.IOException;
import java.util.List;
/**
* Test Class for Reading Each Feature Info inside ShapeFile
*/
public class ShapeFileFeatureReadTest {
@Test
void readFeatureTest() throws IOException {
ShapefileDataStore shapeFileDataStore
= ShapeFileUtil.getShapeFileDataStore(
"src/test/resources/sample/sample.shp",
"UTF-8");
// (1) Shapefile Iterator Loop
// Note: For loop iteration, three steps are needed.
// 1. Extract FeatureSource from dataStore
// 2. Extract FeatureCollection from FeatureSource
// 3. Extract Iterator from FeatureCollection
ContentFeatureSource featureSource = shapeFileDataStore.getFeatureSource();
ContentFeatureCollection featureCollection = featureSource.getFeatures();
try (SimpleFeatureIterator shpFileFeatureIterator = featureCollection.features()) {
while (shpFileFeatureIterator.hasNext()) {
SimpleFeature feature = shpFileFeatureIterator.next();
int attributeCount = feature.getAttributeCount();
List<AttributeDescriptor> attributeDescriptors
= feature.getFeatureType().getAttributeDescriptors();
for (int i = 0; i < attributeCount; i++) {
System.out.println(
attributeDescriptors.get(i).getLocalName() // key
+ " : " + feature.getAttribute(i) // value
);
}
System.out.println("=========================================");
}
}
DataStoreUtil.closeDataStores(shapeFileDataStore);
}
}
package coding.toast.geotools.postgis;
import coding.toast.geotools.utils.DataStoreUtil;
import coding.toast.geotools.utils.PostGisUtil;
import org.geotools.feature.type.GeometryDescriptorImpl;
import org.geotools.jdbc.JDBCDataStore;
import org.geotools.referencing.CRS;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.referencing.FactoryException;
import java.io.IOException;
import java.util.Arrays;
import java.util.Random;
/**
* Reading PostGIS Table Meta Info
*/
public class PostGisMetaDataReadTest {
private static JDBCDataStore postGisDataStore;
@BeforeAll
static void beforeAll() throws IOException {
postGisDataStore = PostGisUtil.getPostGisDataStore(
"postgis", // db type
"localhost", // db server host
"5432", // db server port
"postgres", // database name
"public", // db schema name
"postgres", // db connection user id
"root" // db connection password
);
}
@AfterAll
static void afterAll() {
DataStoreUtil.closeDataStores(postGisDataStore);
}
@Test
void createSimpleGeometryTableTest() throws IOException, FactoryException {
// read All Table inside schema that i config on [PostGisUtil.getPostGisDataSource] method
String[] tableNames = postGisDataStore.getTypeNames();
if (tableNames.length == 0) {
System.out.println("No Table Information found in this database schema : "
+ postGisDataStore.getDatabaseSchema());
return;
}
System.out.println("Database Schema : " + postGisDataStore.getDatabaseSchema());
System.out.println("Table List : " + Arrays.toString(tableNames));
// get Any database Table schema info
int randomIdx = new Random().nextInt(0, tableNames.length);
String randomSelectedTable = tableNames[randomIdx];
SimpleFeatureType tableSchema = postGisDataStore.getSchema(randomSelectedTable);
// read table schema info
for (AttributeDescriptor attributeDescriptor : tableSchema.getAttributeDescriptors()) {
System.out.println("\ntable column name : " + attributeDescriptor.getType().getName());
System.out.println("java attribute Type : " + attributeDescriptor.getType().getBinding().getSimpleName());
if (attributeDescriptor instanceof GeometryDescriptorImpl geometryDescriptor) {
System.out.println("geometry column CRS Name : " + geometryDescriptor.getCoordinateReferenceSystem().getName());
System.out.println("geometry column SRID (=EPSG Code) : " + CRS.lookupEpsgCode(geometryDescriptor.getCoordinateReferenceSystem(), false));
}
System.out.println("get meta info : " + attributeDescriptor.getUserData());
}
}
}
출력 예시
경고! geotools 만으로 생성하는 테이블에는 상당히 많은 약점이 있습니다.
이건 쓰다보면 스스로 느낄 겁니다. 테이블 생성은 최대한 순수한 jdbc 기능만으로
작업하는 게 편합니다.(제 경험상)
package coding.toast.geotools.postgis;
import coding.toast.geotools.utils.DataStoreUtil;
import coding.toast.geotools.utils.PostGisUtil;
import org.geotools.data.DataUtilities;
import org.geotools.feature.AttributeTypeBuilder;
import org.geotools.feature.SchemaException;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.jdbc.JDBCDataStore;
import org.geotools.referencing.CRS;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.locationtech.jts.geom.Point;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.referencing.FactoryException;
import java.io.IOException;
/**
* Geotools table creation test class.
* Warning! Avoid using GeoTools for table creation unless necessary.
* There are many limitations compared to using pure JDBC libraries.
*/
public class CreateTableUsingGeoToolsTest {
private static JDBCDataStore postGisDataStore;
@BeforeAll
static void beforeAll() throws IOException {
postGisDataStore = PostGisUtil.getPostGisDataStore(
"postgis", // db type
"localhost", // db server host
"5432", // db server port
"postgres", // database 명
"public", // db 스키마 명
"postgres", // db connection user id
"root" // db connection 비번
);
}
@AfterAll
static void afterAll() {
DataStoreUtil.closeDataStores(postGisDataStore);
}
// Reading https://docs.geotools.org/stable/userguide/library/main/feature.html
// and https://stackoverflow.com/questions/52554587/add-new-column-attribute-to-the-shapefile-and-save-it-to-database-using-geotools
// and https://gis.stackexchange.com/questions/303709/how-to-set-srs-to-epsg4326-in-geotools
// before this test code will be very helpful!
@Test
@DisplayName("Create table via SimpleFeatureTypeBuilder")
void createTableUsingGeotoolsTest() throws IOException, FactoryException {
SimpleFeatureTypeBuilder featureTypeBuilder = new SimpleFeatureTypeBuilder();
featureTypeBuilder.setName("geotools_create_table"); // table name to create
// setting CRS, if you have multiple geometry columns and want to apply the same
// CRS, this code will be useful.
featureTypeBuilder.setCRS(CRS.decode("EPSG:5186"));
// you can see the comment in the middle of the code below.
// this kind of crs config is useful when you have multiple geometry columns
// and need to configure different crs.
featureTypeBuilder.add("geom", Point.class/*, CRS.decode("EPSG:5186")*/);
// the code below is only useful when you have multiple geometry type columns
featureTypeBuilder.setDefaultGeometry("geom");
// you can create normal attribute info very detail like below
AttributeTypeBuilder nameAttrBuilder = new AttributeTypeBuilder();
nameAttrBuilder.setNillable(false);
nameAttrBuilder.setBinding(String.class);
nameAttrBuilder.setLength(255);
AttributeDescriptor nameAttr = nameAttrBuilder.buildDescriptor("name");
featureTypeBuilder.add(nameAttr);
// name varchar (15) not null
AttributeTypeBuilder ageAttrBuilder = new AttributeTypeBuilder();
ageAttrBuilder.setNillable(true);
ageAttrBuilder.setBinding(Integer.class);
AttributeDescriptor ageAttr = ageAttrBuilder.buildDescriptor("age");
featureTypeBuilder.add(ageAttr);
// ==> age integer
// GeoTools table creation functionality has limitations!
// You can create a column of numeric type using BigDecimal,
// but specifying detailed types like numeric(10,2) is not possible.
// If I'm wrong or if you know a way, please let me know via email.
// Create the table
postGisDataStore.createSchema(featureTypeBuilder.buildFeatureType());
DataStoreUtil.closeDataStores(postGisDataStore);
}
@Test
@DisplayName("Create table via DataUtilities")
void createTableUsingGeotoolsDataUtilsTest2() throws IOException, FactoryException, SchemaException {
String createTableName = "geotools_create_table2"; // table name to create
// Specify the schema of the new table.
// Note (1): typeSpecForPostGIS is a String like "id:java.lang.Long,name:java.lang.String,geom:Point".
// DataUtilities.createType API documentation provides detailed usage, so please refer to it.
// Note (2): By default, a spatial index using GIST is created for the geometry column!
SimpleFeatureType targetSchema
= DataUtilities.createType(createTableName, "id:java.lang.Long,name:java.lang.String,geom:Point");
targetSchema = DataUtilities.createSubType(targetSchema, null, CRS.decode("EPSG:5186"));
// Create the table
postGisDataStore.createSchema(targetSchema);
DataStoreUtil.closeDataStores(postGisDataStore);
}
}
package coding.toast.geotools.postgis;
import coding.toast.geotools.utils.DataStoreUtil;
import coding.toast.geotools.utils.PostGisUtil;
import coding.toast.geotools.utils.ShapeFileUtil;
import org.geotools.data.DataUtilities;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.feature.SchemaException;
import org.geotools.jdbc.JDBCDataStore;
import org.geotools.referencing.CRS;
import org.junit.jupiter.api.Test;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import java.io.IOException;
import java.util.Arrays;
/**
* Test for reading feature attribute information from a shapefile and creating a PostGIS table using that information.
*/
public class CreateTableViaShapeFileTest {
@Test
void test() throws IOException, FactoryException, SchemaException {
// Get ShapefileDataStore from the ShapeFileUtil
ShapefileDataStore shapeFileDataStore = ShapeFileUtil.getShapeFileDataStore(
"src/test/resources/sample/sample.shp",
"UTF-8");
// Get the schema and CRS information from the shapefile
SimpleFeatureType shapeFileSchema = shapeFileDataStore.getSchema();
CoordinateReferenceSystem shapeFileCrs = shapeFileDataStore.getSchema().getCoordinateReferenceSystem();
// Uncommon, but if there's an error in the prj file of the shapefile,
// the EPSG code might not be present. It's better to handle it beforehand.
// If there's no EPSG code, it may cause issues later.
// So, taking preemptive action is advisable; otherwise, you might regret it later!
String userDefaultEpsgCodeInput = "5186";
Integer epsgCode = CRS.lookupEpsgCode(shapeFileCrs, false);
if (epsgCode == null || epsgCode == 0) {
shapeFileDataStore.forceSchemaCRS(CRS.decode("EPSG:" + userDefaultEpsgCodeInput));
}
// Get PostGIS DataStore using PostGisUtil
JDBCDataStore postGisDataStore = PostGisUtil.getPostGisDataStore(
"postgis", // db type
"localhost", // db server host
"5432", // db server port
"postgres", // database name
"public", // db schema name
"postgres", // db connection user id
"root" // db connection password
);
// Specify the table name to be created
String tableToCreate = "new_table";
// Check if the table already exists
boolean isAlreadyExists = Arrays.asList(postGisDataStore.getTypeNames()).contains(tableToCreate);
if (isAlreadyExists) {
System.out.println("Already Existing Table!");
return;
}
// Read attribute information from the shapefile and create the typeSpec string
String typeSpecForPostGIS = PostGisUtil.getTypeSpecForPostGIS(shapeFileSchema);
// Create PostGIS Table Schema
SimpleFeatureType targetSchema
= DataUtilities.createType(tableToCreate, typeSpecForPostGIS);
targetSchema = DataUtilities.createSubType(targetSchema, null, shapeFileCrs);
// Create the table
postGisDataStore.createSchema(targetSchema);
// Close the data stores
DataStoreUtil.closeDataStores(shapeFileDataStore, postGisDataStore);
}
}
package coding.toast.geotools.postgis;
import coding.toast.geotools.utils.PostGisUtil;
import coding.toast.geotools.utils.ShapeFileUtil;
import org.geotools.data.DataStore;
import org.geotools.data.DataUtilities;
import org.geotools.data.DefaultTransaction;
import org.geotools.data.Transaction;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.data.simple.SimpleFeatureStore;
import org.geotools.data.store.ContentFeatureCollection;
import org.geotools.data.store.ContentFeatureSource;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.referencing.CRS;
import org.junit.jupiter.api.Test;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import java.io.IOException;
import java.util.Arrays;
import java.util.Objects;
/**
* Test class for appending shapefile data to a PostGIS table.<br>
* There are a few important points to note here:<br>
* <strong>The target table for data injection must have a numeric primary key.</strong>
* @see <a href="https://sourceforge.net/p/geotools/mailman/geotools-devel/thread/40DF7C1A.1080802%40refractions.net/">Handling tables with no primary</a>
*/
public class ShapeFileToDatabaseTableAppendingTest {
@Test
void appendShapeFileDataToTable() throws IOException, FactoryException {
// Create Shapefile DataStore
ShapefileDataStore shapeFileDataStore = ShapeFileUtil.getShapeFileDataStore(
"src/test/resources/sample/sample.shp",
"UTF-8");
// this shapefile have 3 attributes
// id(number)
// name(string)
// geom(Point, 5186)
// Extract CRS in advance
CoordinateReferenceSystem shapeFileCrs = shapeFileDataStore.getSchema().getCoordinateReferenceSystem();
// Uncommon, but if there's an error in the prj file of the shapefile,
// the EPSG code might not be present. It's better to handle it beforehand.
// If there's no EPSG code, it may cause issues later. So, taking preemptive action is advisable.
String userDefaultEpsgCodeInput = "5186";
Integer epsgCode = CRS.lookupEpsgCode(shapeFileCrs, false);
if (epsgCode == null || epsgCode == 0) {
shapeFileDataStore.forceSchemaCRS(CRS.decode("EPSG:" + userDefaultEpsgCodeInput));
}
// Create PostGIS DataStore
DataStore postGisDataStore = PostGisUtil.getPostGisDataStore(
"postgis",
"localhost",
"5432",
"postgres",
"public",
"postgres",
"root"
);
// Target table name for data insert
String targetTableName = "data_insert_test";
// Caution! There must be a numeric primary key in the table where you want to insert data!!!
/*
-- table DDL
create table public.data_appending_test
(
fid integer not null
constraint new_table_pkey
primary key,
id bigint,
name varchar,
geom geometry(Point, 5186)
);
-- create index spatial_new_table_geom
-- on public.data_appending_test using gist (geom);
*/
// Check if the table really exists in the database
if (!Arrays.asList(postGisDataStore.getTypeNames()).contains(targetTableName)) {
System.err.println("No Table Found!!!!!!");
}
// Create a new schema based on the existing postGIS schema
// Retrieve the FeatureType (= schema) of a specific table.
SimpleFeatureType postGisSchema = postGisDataStore.getSchema(targetTableName);
SimpleFeatureTypeBuilder builderForPostGIS = new SimpleFeatureTypeBuilder();
builderForPostGIS.setName(postGisSchema.getName());
builderForPostGIS.setSuperType((SimpleFeatureType) postGisSchema.getSuper());
builderForPostGIS.addAll(postGisSchema.getAttributeDescriptors());
// Note1: postGisSchema.getAttributeDescriptors() retrieves only columns excluding the primary key.
// Note2: If needed, add additional attributes like builderForPostGIS.add("new_attr", String.class);
// However, this should be a column actually present in the PostGIS table!
// Create a FeatureType containing column information for the postGIS table
SimpleFeatureType postGISFeatureType = builderForPostGIS.buildFeatureType();
// Declare a transaction; assignment will be done inside the try-catch block.
Transaction transaction = null;
// Amount of data to be sent to the database at once
final int BATCH_SIZE = 1000;
// Counting the number of data accumulating in one transaction
int count = 0;
try {
// Set the transaction to the FeatureStore where we want to perform the write operation.
transaction = new DefaultTransaction("POSTGIS_DATA_APPENDING");
// Extract the FeatureSource where the actual data will be inserted
SimpleFeatureSource tableFeatureSource = postGisDataStore.getFeatureSource(targetTableName);
// typename == table name specified
// Casting SimpleFeatureSource to SimpleFeatureStore for setting the transaction
// A crucial caution!
// If there is no numeric primary key in the table where you want to insert data,
// an error will occur in the following Class Casting! Make sure to check the presence
// of a numeric primary key in the table where you want to insert data!
SimpleFeatureStore tableFeatureStore = (SimpleFeatureStore) tableFeatureSource;
tableFeatureStore.setTransaction(transaction);
// Create an iterator to read features from the shapefile
ContentFeatureSource featureSource = shapeFileDataStore.getFeatureSource();
ContentFeatureCollection featuresCollection = featureSource.getFeatures();
try (SimpleFeatureIterator features = featuresCollection.features()) {
// Start iterating through the features
while (features.hasNext()) {
SimpleFeature shapeFileFeature = features.next();
SimpleFeature transformedFeature = DataUtilities.template(postGISFeatureType);
// Warning! Never set Attribute for a database table numeric primary key!
// transformedFeature.setAttribute("fid", shapeFileFeature.getAttribute("????")); ==> don't do this!
// transform shapefile data to table data
transformedFeature.setAttribute("id", shapeFileFeature.getAttribute("id"));
transformedFeature.setAttribute("name", shapeFileFeature.getAttribute("name"));
transformedFeature.setDefaultGeometryProperty(shapeFileFeature.getDefaultGeometryProperty());
tableFeatureStore.addFeatures(DataUtilities.collection(transformedFeature));
count++;
if (count % BATCH_SIZE == 0) {
transaction.commit();
transaction.close();
tableFeatureStore.setTransaction(null);
transaction = new DefaultTransaction("POSTGIS_DATA_APPENDING");
tableFeatureStore.setTransaction(transaction);
}
}
// Features added with tableFeatureStore.addFeature might not have been committed yet even
// after the while loop. Commit them.
if ((count % BATCH_SIZE) != 0) {
transaction.commit();
}
}
} catch (Exception e) {
if (Objects.nonNull(transaction)) transaction.rollback();
e.printStackTrace(System.err);
} finally {
if (Objects.nonNull(transaction)) transaction.close();
postGisDataStore.dispose();
shapeFileDataStore.dispose();
}
}
}
데이터 입력 후 QGIS 에서 PostGIS 테이블을 조회하면 아래처럼
point 정보가 지도에 표출됩니다. 참고로 좌표계에 맞게 데이터가 들어간 덕에
이렇게 우리나라 Boundary 안에 Point 가 찍히는 겁니다!
참고:
만약에 위 코드가 너무 유치하다고 생각하시는 분들을 위해서,
연속지적도 데이터를 업로드 할 수 있는 코드를 따로 작성해봤습니다.
소스 코드 보기
메모리는 모니터링을 해봤는데, 다행히 메모리가 튀거나 하지도 않네요.
음... batch 사이즈를 1000 으로 하건, 10000 으로 하건...
느립니다! GDAL 에 비해서 턱없이 느립니다.~`
~~제가 코딩을 좀 잘못한 걸지도 모르지만... 그렇다 해도 좀 심한 거 같습니다.
지금 연속지적도_서울 (총 Feature 수 : 90만개)을 돌려보는데 현재 40분째 돌리는데
24만개의 데이터가 들어가네요.
혹시 해결법 아시는 분 있으면 댓글 부탁드립니다. 커피 쏩니다 😂
ps. 2024-06-10
이번에육육
이라는 아이디의 개발자분께서 댓글을 남기시고
직접 테스트를 해보셨는데, 그렇게 오래 걸리지 않는다는 말씀을 해주셨는데,
실제로 저도 오늘 개인 PC 에서 돌려보니 6분 밖에 안 걸리더군요.코드를 실행하는 환경에 따라 다른 것이라 생각도 됩니다만,
안탑깝게도 느리게 동작하던 당시의 테스트 환경을 일일이 기억하지는 못하는지라,
정확한 이유는 모르겠네요. 아무튼 속도는 무난한 거 같습니다!
딱 잘라 결론부터 말하겠습니다.
GeoTools 를 통해서 데이터를 넣으려는
DB Table
은
반드시 한 개 이상의Primary Key
를 갖고 있어야 합니다.
제가 위 4번 목차에서 테스트한 ShapeFile 과 DB Table 의 구조를
보면서 설명을 해보겠습니다..
먼저 테스트용 shapefile 은 다음과 같은 Feature 속성이 있습니다.
테스트용 PostGIS Table DDL
은 다음과 같습니다.
create table public.data_appending_test
(
fid integer primary key, -- serial 타입이여도 상관없음!
id bigint,
name varchar,
geom geometry(Point, 5186)
);
create index spatial_data_appending_test_geom
on public.data_appending_test using gist (geom);
만약에 제가 여기서 DB Table 의 primary key(=fid)
를 지우면 어떻게 될까요??
예외가 발생합니다 !
예외 발생 코드는 아래와 같습니다.
코드
:
SimpleFeatureStore tableFeatureStore = (SimpleFeatureStore) tableFeatureSource;
에러 메시지
:
java.lang.ClassCastException: class org.geotools.jdbc.JDBCFeatureSource cannot be cast to class org.geotools.data.simple.SimpleFeatureStore
아주 세세한 이유는 모르겠지만,
primary key 가 단 하나도
없으면 100% 이런 예외를 뱉습니다.
그렇기 때문에 (계속 말씀드리지만)
반드시 data 를 넣으려는 테이블에 primary key 가 최소 1개 이상은 있어야 합니다.
일단 제가 테스트해본 결과로는 2가지 Type 을 조합 또는 복수로 PK 로 지정할 수 있습니다.
아, 물론 복수가 아닌 1개의 column 으로 PK 를 잡아도 잘됩니다.
예를 들어서, 숫자 2개를 조합한 테이블을 아래처럼 생성하고,
위 목차 4번의 코드를 돌려보겠습니다.
create table public.geotools_pk_test1
(
fid bigint,
fid2 integer,
id bigint,
name varchar,
geom geometry(Point, 5186)
);
alter table public.geotools_pk_test1
add constraint geotools_pk_test1_mixed_pk primary key (fid, fid2);
데이터 insert 결과:
create table public.geotools_pk_test2
(
fid bigint,
fid2 varchar, -- 길이 제한을 주고 싶다면, 최소 31 로 해야합니다.
id bigint,
name varchar,
geom geometry(Point, 5186)
);
alter table public.geotools_pk_test2
add constraint geotools_pk_test2_mixed_pk primary key (fid, fid2);
데이터 insert 결과:
create table public.geotools_pk_test3
(
fid varchar,
fid2 varchar,
id bigint,
name varchar,
geom geometry(Point, 5186)
);
alter table public.geotools_pk_test3
add constraint geotools_pk_test3_mixed_pk primary key (fid, fid2);
데이터 insert 결과:
제가 GDAL 을 개인 PC 에서 주로 사용하고,
회사에서도 많은 공간 데이터들을 GDAL 을 통해서 PostgreSQL DB 로 업로드 하다보니,
원치 않게 GDAL 과 Geotools 를 계속 비교하게 되네요.
제가 생각하는 ShapeFile + PostGIS 관련 기능에서 GeoTools 의 불편한 점을 작성해봅니다.
이미 말했지만, 데이터를 넣으려는 테이블에 반드시 PK 가 있어야 된다는 점이...
솔직히 좀 불편합니다. GDAL 에서는 딱히 이런 제약은 없었거든요.
그리고 Table 생성할 때도 numeric
같은 타입에 더 상세하게
numberic(10,2)
이런 식으로 타입을 지정하고 싶어도,
이건 방법 자체를 GeoTools 에서 제공하지 않더군요 (제가 찾아본 바로는!).
진짜 조금조금씩 애매하게 기능을 제공하는 느낌을 받았습니다.
위에서 제가 테이블 data insert 시에 반드시 PK 가 있어야 된다고 한 건 기억하죠?
그런데 사실 이거 문제 해결하는데 5시간 걸렸습니다 (그것도 주말에...)
이 말은? 검색을 해도 여러분이 원하는 해결법을 찾는 건 정말 하늘에 별따기 라는 겁니다.
StackOverFlow 를 계속 돌아다녀서 운좋게 해결법을 찾는다 해도 족히 몇 시간은 걸릴 겁니다.
심지어 어떤 건 아예 찾지 못해서 결국 포기하는 경우도 빈번할 겁니다.
이건 직접 코딩하다보면 많이 느끼게 되실거라 생각합니다.
물론 GeoTools 프로젝트 email list 를 통해서 이메일을 보내면 되지만...
영어로 보내야 하며, 답장을 꼭 주리라는 보장이 없습니다.
아 그리고 "ChatGPT 쓰면 되지 않냐?"는 분들을 위해 한마디 하자면,
ChatGPT 가 힌트를 주려고 노력은 하지만, 존재하지도 않는 API 를 사용하라고
계속 코드 예시를 보여줍니다. 여러분들도 한번 ChatGPT 사용해서 해보시기 바랍니다.
답변은 잘쓰는데, 코드로 옮기면 IDE 가 빨간줄을 뱉어낼 겁니다.
GeoTools 와 관련된 정보를 인터넷에서 찾아보는 거 자체가 참 힘든 거 같습니다.
GDAL 은 CLI 에서 쓰기 위해서 최적화되다 보니
어찌보면 당연한 걸 수도 있지만, 그렇다 해도 GeoTools 는 너무나도 장황합니다.
GeoTools 에서 datastore A => datastore B
같이 간단하게 데이터를
옮기는 코드를 좀 더 추상화한 API 를 제공하면 좋지 않을까 싶네요.
이 부분은 지극히 제 개인적인 생각입니다.
추후에 제가 GeoTools 를 더 잘 쓰게 되면 달라질 수도 있겠죠?
하지만 지금은? 어림없죠!GDAL
이 최곱니다.
솔직히 제가 GeoTools
를 완벽하게 아는 것도, 쓸 줄 아는 것도 아니지만...
개인적으로 ShapeFile
, PostGIS
에 대한 처리는 GDAL
을 쓰는 게 좋은 거 같습니다.
특히 현업에서는 더더욱 유연하고 다양한 기능 구현이 필요하기에,
GDAL 이 좀 더 맞지 않나 싶네요.
그리고 다음 인수인계를 할때도 GDAL 을 통해 구현하면 방대한 GDAL 관련 정보를 인터넷에서
아주 쉽게 찾을 수 있어서 유지보수에도 많은 도움이 될듯합니다.
그렇다면 GDAL 을 Java 에서 사용할 수 있는 방법이 없을까요?
있습니다!
gdal-java
zt-exec
+ gdal binary
gdal
설치 하는 것 자체가 Naver D2 - Java에서 외부 프로세스를 실행할 때
게시물을 한번 읽고 시작하는 것을 추천합니다. 의외로 놓치기 쉬운 부분에 대해서 자세히 알려줍니다.이번 글은 여기까지만 작성하겠습니다.
어휴 주말에 이거 조사하느라 쉬지도 못했네요