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データを入力として処理を行う別の仕組みとして考えます。
整理すると、
という関係です。
この形にしておけば、業務が変わっても基本的なデータ構造を共通化できます。
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を置きます。
これにより、
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圏
という処理を考えます。
この形なら、
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モデル
ここまでをまとめると、最初のデータモデルは次のようになります。
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全体構成とつなげると次のようになります。
ここまで整理できると、OpenLayers、FastAPI、Celery、PostGISの役割もかなり明確になります。
25. 今回の設計方針
今回の汎用WebGISでは、次の考え方を基本にします。
-
Workspace → Layer → FeatureをGISデータの基本階層にする - FeatureがGeometryとAttributesを持つ
- 自由度の高いLayerでは
properties JSONBを利用できるようにする - 件数が多いデータや重要なデータは専用PostGISテーブルを使えるようにする
- Job / ResultはGISデータの階層とは分けて管理する
- Jobの結果から新しいLayerを作れるようにする
- FlatGeobufはPostGISから生成する表示用データとして扱う
- 必要に応じて編集履歴や同時編集対策を追加する
特に重要なのは、汎用テーブルだけに寄せすぎないことです。
最初はgis.features + JSONBで小さく始め、データ量や業務要件が増えたLayerだけ専用テーブルへ移せるようにしておくと、後から拡張しやすくなります。
26. 次に考えること
データモデルの方針が決まったら、次は実際のPostgreSQL / PostGISテーブルを作っていきます。
次の段階では、
CREATE SCHEMACREATE TABLE- Foreign Key
- GiST Index
- JSONB用Index
- 初期データ
- Migration
- FastAPIからの接続
まで具体化します。
ここまでできれば、OpenLayersからFeatureを登録・編集し、FastAPI経由でPostGISへ保存するところまで進められます。
次回は、
「PostgreSQL / PostGISで汎用WebGISのテーブルを実装する」
ところから進めます。
参考リンク集
- OpenLayers https://openlayers.org/
- Turf.js https://turfjs.org/
- FlatGeobuf https://flatgeobuf.org/
- FastAPI https://fastapi.tiangolo.com/
- PostgreSQL https://www.postgresql.org/
- PostGIS https://postgis.net/
- Celery https://docs.celeryq.dev/
- Redis https://redis.io/
- Docker Compose https://docs.docker.com/compose/
- Nginx https://nginx.org/





