1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Docker + OpenLayers + Turf.js + FlatGeobuf + FastAPI + PostGIS + Celery で作る汎用WebGIS基盤2 ー データモデル設計

1
Posted at

Docker + OpenLayers + Turf.js + FlatGeobuf + FastAPI + PostGIS + Celery で作る汎用WebGIS基盤2 ー データモデル設計

はじめに

前回は、Docker、OpenLayers、Turf.js、FlatGeobuf、FastAPI、PostgreSQL / PostGIS、Celery、Redis、Nginx、Dockerを組み合わせたWebGISの全体構成を整理しました。

次に考えたいのが、WebGISの中で扱うデータをどう持つかです。

地図上に表示するデータは、単純に「点・線・面」だけではありません。実際には、レイヤ、地物、属性、Geometry、処理条件、Job、処理結果、編集履歴など、いくつかの種類に分かれます。

ここを最初に整理しておかないと、機能が増えたときにテーブル構成が分かりにくくなります。

今回は、道路管理、防災、設備管理、河川管理、現地調査など、特定の業務に依存しないWebGISを前提に、PostgreSQL / PostGIS のデータモデルを考えます。


1. 最初にデータの関係を整理する

最初は次のような流れを考えていました。

Layer
  ↓
Feature
  ↓
Attribute
  ↓
Geometry
  ↓
Job
  ↓
Result

概念としては分かりやすいのですが、実際のデータベース設計では少し考え方を変えた方が自然です。

AttributeとGeometryはFeatureの次に来るものではなく、Featureを構成する情報です。

また、JobやResultはFeatureの下に置くのではなく、GISデータを入力として処理を行う別の仕組みとして考えます。

20260912_d01.png

整理すると、

20260912_d02.png

という関係です。

この形にしておけば、業務が変わっても基本的なデータ構造を共通化できます。


2. PostgreSQLのSchemaを分ける

PostgreSQLでは、すべてのテーブルをpublicに置くこともできます。

ただ、WebGISとして機能が増えてくると、用途ごとにSchemaを分けた方が管理しやすくなります。

PostgreSQL / PostGIS
│
├── app
│   ├── workspaces
│   ├── layers
│   ├── layer_fields
│   ├── layer_styles
│   └── layer_publications
│
├── gis
│   ├── features
│   └── 業務別GISテーブル
│
├── job
│   ├── jobs
│   ├── job_inputs
│   └── job_results
│
└── audit
    └── feature_history

役割を分けると、それぞれのテーブルが何のためにあるのか分かりやすくなります。


3. WorkspaceはWebGISのまとまり

汎用的なWebGISを作るなら、Layerの上にWorkspaceのような単位を置いておくと便利です。

例えば、

○○市 防災GIS
├── 避難所
├── 河川
└── ハザード区域

○○県 道路管理GIS
├── 道路
├── 橋梁
└── 点検箇所

という形です。

app.workspacesには、例えば次のような項目を持たせます。

カラム 内容
id Workspace ID
name 名称
description 説明
created_at 作成日時
updated_at 更新日時

名称はProjectでも構いませんが、今回は複数用途へ広げやすいようにWorkspaceという名前で考えます。


4. LayerはGISデータのまとまり

Layerは、地図上で扱うデータの単位です。

例えば、道路、河川、建物、避難所、行政界、ハザード区域などです。

Layer自身に大量のGeometryを持たせるのではなく、Layerはデータの管理情報を持つものと考えます。

例えばapp.layersには、次のような項目を持たせます。

カラム 内容
id Layer ID
workspace_id 所属Workspace
name 表示名称
description 説明
geometry_type Point / LineString / Polygon など
srid 座標系
storage_type 保存方式
schema_name 実データSchema
table_name 実データTable
editable 編集可能か
visible 表示対象か
created_at 作成日時
updated_at 更新日時

例えば、

名称        : 避難所
Geometry    : Point
保存方式    : generic
保存先      : gis.features
編集可能    : true

というような情報を持たせます。

SRIDについては対象地域や元データによって変わるため、ここでは固定しません。


5. FeatureがGISデータ本体

Layerの中に存在する1件1件の地物がFeatureです。

例えば「避難所」というLayerがある場合、

避難所Layer
├── ○○小学校
├── △△公民館
└── □□体育館

この1件1件がFeatureです。

汎用Featureテーブルを用意する場合は、例えば次のような形です。

gis.features

id
layer_id
geom
properties
created_at
updated_at
version

Geometryと属性情報をFeature単位で管理します。


6. FeatureはGeometryとAttributesを持つ

Featureの構造は次のように考えます。

Feature
├── Geometry
└── Attributes

GeometryはPostGISのgeometry型を使います。

例えば、

geom geometry(Geometry, <SRID>)

のような形です。

一方、属性は次のような情報です。

{
  "name": "○○小学校",
  "capacity": 500,
  "category": "指定避難所"
}

ここで問題になるのが、属性をどのように保存するかです。


7. 属性の保存方法

方法1:通常のカラムで持つ

id
name
capacity
category
geom

この方法は分かりやすく、SQLも書きやすいです。

一方で、Layerごとに項目が違うため、汎用WebGISではLayerごとにテーブルを作る必要があります。

方法2:JSONBで持つ

id
layer_id
properties JSONB
geom

属性は、

{
  "name": "○○小学校",
  "capacity": 500,
  "category": "避難所"
}

のように保存します。

自由に項目を追加できるため、利用者が新しいLayerを追加できるようなWebGISとは相性がよい方法です。

方法3:属性を1項目ずつ行として持つ

feature_id
attribute_name
attribute_value

例えば、

001 | name     | ○○小学校
001 | capacity | 500
001 | category | 避難所

のような形です。

柔軟ではありますが、SQLが複雑になりやすく、型管理や集計もしにくくなります。

そのため、基本構成としては積極的には使わない方針にします。


8. 汎用テーブルと専用テーブルを使い分ける

実際のシステムでは、すべてをJSONBにする必要はありません。

逆に、すべてのLayerに専用テーブルを作る必要もありません。

そこで、両方を使い分けます。

小~中規模・自由定義Layer
        ↓
gis.features
properties JSONB

大規模・重要Layer
        ↓
専用PostGIS Table

例えばgis.featuresには、現地調査ポイント、メモ、ユーザー作成Polygon、簡易的な業務Layerなどを保存します。

一方で、

gis.roads
gis.buildings
gis.facilities

のように、件数が多いデータや業務上重要なデータは専用テーブルにします。

最初からすべてを専用設計にしないので、小さく始めやすいのがこの方法の利点です。


9. Layer側で保存先を管理する

汎用テーブルと専用テーブルを使い分ける場合は、Layer側で保存先を管理します。

app.layersに、

storage_type
schema_name
table_name

を持たせます。

例えば、

避難所

storage_type = generic
schema_name  = gis
table_name   = features

一方、道路の場合は、

道路

storage_type = table
schema_name  = gis
table_name   = roads

のようにします。

FastAPIはLayer情報を見て、どのテーブルを参照すればよいか判断できます。


10. 属性項目の定義も管理する

properties JSONBを使う場合、データを見ただけではどんな属性項目が存在するのか分かりにくくなります。

そこで、app.layer_fieldsを用意します。

例えば避難所なら、

name
capacity
category
address
telephone

といった項目定義を持たせます。

カラム 内容
id ID
layer_id Layer
field_name DB上の項目名
label 画面表示名称
data_type string / integer など
required 必須か
editable 編集可能か
display_order 表示順
validation 入力条件

この定義をFastAPIから取得すれば、フロント側で入力フォームを自動生成することもできます。

PostgreSQL
 ↓
layer_fields
 ↓
FastAPI
 ↓
Web画面
 ↓
入力フォーム生成

汎用WebGISとしては、この仕組みがあるとかなり使いやすくなります。


11. StyleもLayer単位で管理する

WebGISでは、何を表示するかだけでなく、どう表示するかも重要です。

そこで、app.layer_stylesを用意します。

例えば、

{
  "fillColor": "#ff0000",
  "fillOpacity": 0.4,
  "strokeColor": "#880000",
  "strokeWidth": 2
}

のような情報を持たせます。

OpenLayers側でこの情報を読み込み、描画Styleに反映します。

将来的には、単一色、カテゴリ別表示、数値による段階色、アイコン表示、ラベル表示などへ拡張できます。


12. Geometryには空間Indexを付ける

PostGISで空間検索を行う場合、Geometryには空間Indexを付けます。

例えば、

CREATE INDEX idx_features_geom
ON gis.features
USING GIST (geom);

という形です。

これにより、

ST_Intersects
ST_Within
ST_Contains
ST_DWithin

などの空間検索を効率よく実行できます。


13. JobはGISデータと別に考える

GISシステムでは、データの編集だけでなく、処理を実行する機能も必要になります。

例えば、

  • Buffer生成
  • 空間結合
  • 集計
  • 座標変換
  • FlatGeobuf生成
  • シミュレーション
  • 大量データ変換

などです。

これらはFeatureの下に置かず、Jobとして別に管理します。

Job
  ↓
Input

という関係です。


14. jobsテーブル

例えばjob.jobsには次のような情報を持たせます。

id
job_type
status
parameters
requested_by
progress
queued_at
started_at
finished_at
error_message

Buffer処理なら、

{
  "job_type": "buffer",
  "status": "running",
  "parameters": {
    "distance": 100
  }
}

のようになります。


15. JobとLayerは多対多で考える

GIS処理では、複数のLayerを使う場合があります。

例えば、

避難所
  +
浸水区域
  ↓
空間解析

といった処理です。

そのため、JobとLayerの間にjob_inputsを置きます。

20260912_d03.png

これにより、

Job A
├── Layer 1
└── Layer 2

という構成を持てます。


16. Resultも柔軟に持たせる

GIS処理の結果は、必ずしも同じ形ではありません。

例えば、

  • 数値
  • JSON
  • Geometry
  • 新しいLayer
  • ファイル
  • レポート

などがあります。

そのため、job.job_resultsには、

id
job_id
result_type
result_layer_id
summary
created_at

のような項目を持たせます。

例えばBuffer処理で新しいLayerができた場合は、

Job
 ↓
Result
 ↓
新しいLayer

という関係になります。


17. Jobの結果から新しいLayerを作る

例えば、

避難所
  ↓
100m Buffer
  ↓
避難所100m圏

という処理を考えます。

20260912_d04.png

この形なら、

Layer A
 ↓
Job
 ↓
Layer B
 ↓
Job
 ↓
Layer C

という連続したGIS処理にもつなげられます。

将来的にGISワークフローを作る場合にも利用しやすい構造です。


18. FlatGeobufもLayerから作られる派生データ

FlatGeobufは表示を高速化するためのデータです。

正式な保存先はPostgreSQL / PostGISなので、FlatGeobufはPostGISから再生成できるようにします。

PostGIS Layer
   ↓
Celery
   ↓
FlatGeobuf

必要であれば、app.layer_publicationsのような管理テーブルを用意します。

例えば、

layer_id
format
path
generated_at
source_version
status

などです。

これにより、

Layer更新
   ↓
FlatGeobufが古くなる
   ↓
再生成
   ↓
最新版へ更新

という流れを管理できます。


19. 編集履歴も考えておく

複数人で使うWebGISでは、

誰が
いつ
何を
変更したか

が必要になることがあります。

そこで必要に応じてaudit.feature_historyを用意します。

例えば、

feature_id
operation
before_data
after_data
user_id
changed_at

などです。

操作としてはINSERT、UPDATE、DELETEを記録できます。

ただし、履歴をどこまで保存するかはシステムごとに違うため、必須にはしません。


20. 同時編集に備えてversionを持たせる

複数人で使う場合、同じFeatureを同時に編集する可能性があります。

そのためFeatureにversionを持たせる方法があります。

編集開始
version = 5

保存時
DB version = 5 ?
   ↓
YES → version 6で更新
NO  → 他のユーザーが先に更新済み

単純な上書きを防ぐための仕組みとして使えます。


21. 大量データになったらPartitionを検討する

最初からPartitionを導入する必要はありません。

まずは通常のTableとIndexで始めます。

その後、

数千万Feature
大量のJob履歴
大量の時系列

といった規模になった段階でPartitionを検討します。

最初
 ↓
Table + Index

データ増加
 ↓
実測

必要になったら
 ↓
Partition

という流れの方が管理しやすくなります。


22. 全体ERモデル

ここまでをまとめると、最初のデータモデルは次のようになります。

20260912_d05.png


23. PostgreSQL / PostGISの構成

Schema単位で整理すると、次のような構成になります。

PostgreSQL / PostGIS
│
├── app
│   ├── workspaces
│   ├── layers
│   ├── layer_fields
│   ├── layer_styles
│   └── layer_publications
│
├── gis
│   ├── features
│   └── dedicated tables
│       ├── roads
│       ├── facilities
│       └── ...
│
├── job
│   ├── jobs
│   ├── job_inputs
│   └── job_results
│
└── audit
    └── feature_history

最初からすべてを作る必要はありません。

必要になった機能から順番に追加していけばよいと思います。


24. WebGIS全体ではどうつながるか

データモデルを、WebGIS全体構成とつなげると次のようになります。

20260912_d06.png

ここまで整理できると、OpenLayers、FastAPI、Celery、PostGISの役割もかなり明確になります。


25. 今回の設計方針

今回の汎用WebGISでは、次の考え方を基本にします。

  1. Workspace → Layer → FeatureをGISデータの基本階層にする
  2. FeatureがGeometryとAttributesを持つ
  3. 自由度の高いLayerではproperties JSONBを利用できるようにする
  4. 件数が多いデータや重要なデータは専用PostGISテーブルを使えるようにする
  5. Job / ResultはGISデータの階層とは分けて管理する
  6. Jobの結果から新しいLayerを作れるようにする
  7. FlatGeobufはPostGISから生成する表示用データとして扱う
  8. 必要に応じて編集履歴や同時編集対策を追加する

特に重要なのは、汎用テーブルだけに寄せすぎないことです。

最初はgis.features + JSONBで小さく始め、データ量や業務要件が増えたLayerだけ専用テーブルへ移せるようにしておくと、後から拡張しやすくなります。


26. 次に考えること

データモデルの方針が決まったら、次は実際のPostgreSQL / PostGISテーブルを作っていきます。

次の段階では、

  • CREATE SCHEMA
  • CREATE TABLE
  • Foreign Key
  • GiST Index
  • JSONB用Index
  • 初期データ
  • Migration
  • FastAPIからの接続

まで具体化します。

ここまでできれば、OpenLayersからFeatureを登録・編集し、FastAPI経由でPostGISへ保存するところまで進められます。

次回は、

「PostgreSQL / PostGISで汎用WebGISのテーブルを実装する」

ところから進めます。


参考リンク集

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?