Rails で PostGIS を使う方法（Docker, Heroku）

Rails アプリケーションで、経度緯度をつかって位置情報を使い時がありました。

PostGIS は PostgreSQL の拡張です。これを使うと、その経度緯度をデータベースのカラムに入れることができて、また 2 点間の距離を計算したりすることができるようです。

この記事では、

- ローカル開発環境を Docker でつくる
- heroku で動くように設定する

の２つをかきます。

# ローカル開発環境を Docker でつくる

やることは

1. postgis のイメージをつかっちゃう
2. activerecord-postgis-adapter という gem つかう
3. database.yml の設定
4. `rake db:create` を実行する
5. model と migration をつくって動かしてみる

イメージとして、とても簡単な以下のテーブルをつくってみます。

**stations** テーブル

| name       | lonlat |
|:-----------|:-----------|
| 渋谷        | 経度緯度の情報 |
| 新宿        | 経度緯度の情報 |

感想: PoistGIS を知る前は、 `longitude(経度）` と `latitude(緯度)` をそれぞれカラムで持つのかなぁと思っていたのですが。 PostGIS を使うと、上のように `lonlat` というひとまとめにしたデータを持つことができます。

## Step.1 Image

ローカル開発環境は、docker-compose で用意しています。

なので、端折って書きますが。以下のようにイメージを指定すると完了です。

```diff:docker-compose.yml
version '3'
services:
  app:
  db:
-    image: postgres:10
+    image: mdillon/postgis:10
  kvs:
```

PostGIS のイメージは [こちら](https://wiki.osgeo.org/wiki/DockerImages) にまとまっていますが、その中から今回は上を選択しました。

## Step.2 Gem

```rb:Gemfile
gem 'activerecord-postgis-adapter'
```

こちらの gem を使います。
https://github.com/rgeo/activerecord-postgis-adapter

以降の Step.3-5 は基本的に `activerecord-postgis-adapter` のドキュメントに書いてある通りですので、詳細はそちらを参照ください。

## Step.3 database.yml

```diff:database.yml
default: &default
-  adapter: postgresql
+  adapter: postgis
```

## Step.4 Command

今回は、すでに PostgreSQL で諸々環境を用意したあとに、PostGIS に切り替えていますので、ドキュメントにある通り以下のコマンドを叩きました。

```
$ rake db:gis:setup
```

## Step.5 Migration

簡単な migration をつくります。

```rb:create_stations.rb
class CreateStations < ActiveRecord::Migration[5.1]
  def change
    create_table :stations do |t|
      t.string :name
      t.st_point :lonlat, geographic: true
      t.timestamps
    end
  end
end
```

```rb:Station.rb
class Station < ApplicationRecord
end
```

これで、 `rails console` で、動作を確認してみます。

```rb:rails_console
# データ作成は、直感的です。
pry(main)> Station.create(name: "hoge", lonlat: 'POINT(30 40)')


# こんな感じで、ある場所から近いところを取れることを確認しました。
pry(main)> Station.where("ST_Distance(lonlat, 'POINT(30 50)') < 100000000").order("ST_Distance(lonlat, 'POINT(30 50)')")

=>   Station Load (0.8ms)  SELECT "stations".* FROM "stations" WHERE (ST_Distance(lonlat2, 'POINT(30 50)') < 100000000) ORDER BY ST_Distance(lonlat2, 'POINT(30 50)')
[#<Station:0x0000000004fe0840
  id: 2,
  name: "piyo",
  lonlat1: #<RGeo::Cartesian::PointImpl:0x283f714 "POINT (30.0 40.0)">,
  lonlat2: #<RGeo::Geographic::SphericalPointImpl:0x2830f84 "POINT (30.0 40.0)">,
  created_at: Mon, 15 Jan 2018 19:03:43 JST +09:00,
  updated_at: Mon, 15 Jan 2018 19:03:43 JST +09:00>,
 #<Station:0x0000000004fe0688
  id: 1,
  name: "hoge",
  lonlat1: #<RGeo::Cartesian::PointImpl:0x2882474 "POINT (-122.0 46.0)">,
  lonlat2: #<RGeo::Geographic::SphericalPointImpl:0x28a2878 "POINT (50.0 1.0)">,
  created_at: Mon, 15 Jan 2018 19:02:49 JST +09:00,
  updated_at: Mon, 15 Jan 2018 19:02:49 JST +09:00>]
```

以上で、ローカル環境は整ったと思います。

# heroku で動かす

cf. heroku document 
https://devcenter.heroku.com/articles/postgis

heroku で動かすには、（すこしはまったのですが）以下の対応をすると動きました。


- postgres を Add-on でいれておく
- [heroku-geo-buildpack](https://github.com/cyberdelia/heroku-geo-buildpack) という Buildpack を入れる
- database.yml を書き換える （<= これにハマりましたが @namitop が解決策を教えてくれました 😁


Add-on と Buildpack は普通に入ると思います。

一応 [heroku-geo-buildpack](https://github.com/cyberdelia/heroku-geo-buildpack) には以下の順番で入っていたので、その順番にあわせてあります。

```
$ heroku buildpacks
=== sushi Buildpack URLs
1. https://github.com/cyberdelia/heroku-geo-buildpack.git
2. heroku/ruby
```


## database.yml の書き換え

これに気づかなくて大変でした。

```diff:database.yml
production:
  <<: *default
  database: eikaiwa_production
+  url: <%= ENV.fetch('DATABASE_URL', '').sub(/^postgres/, "postgis") %>
```

該当の heroku document はこちらです。
https://devcenter.heroku.com/articles/rails-database-connection-behavior#active-record-4-1-escape-valve


以上で、heroku でも動きました。

まだ、PostGIS は出会ったばかりで、距離計算とかの仕様をこれから勉強する状況ですが、とても楽しそうです 🍏

ありがとうございました。