【Rails】gem geocoderを使用する際のバリデーションについて

## はじめに
こんにちは、なかじ(@nakayama-bird)と申します。
現在、プログラミングスクールに通ってRuby on Railsを学習しております。

:::note warn
私はプログラミング学習中で、初学者です。
内容に誤りのある場合がございます。
もし間違いがあればご指摘いただけますと幸いです。
:::
## 記事の概要
現在、ポートフォリオの作成をしております。
地図上で投稿した場所が表示されるように住所から緯度・経度の座標を取得する際に、モデルでの設定でつまづいた箇所についてまとめてみました。
## 環境
- Rails 7.1.3.4
- gem geocoder
- Google Maps API(Geocoding API)
## gem geocogerの導入・設定方法
### 1. 導入に至った経緯
今回のアプリケーションではユーザーがある場所を投稿して、それを地図上にピンで表示するという機能を実装しています。ピンで表示する際、緯度・経度の情報が必要になるのですが、それをユーザーに直接入力してもらうのは現実的でありません。そのため、住所から緯度・経度の情報を呼び出すことができるgem geocogerの導入を決めました。

https://github.com/alexreisner/geocoder

### 2. gemの導入の流れ
```ruby:Gemfile
gem 'geocoder'
```
Gemfileに追記したら、`bundle install`でGemをインストールします。
導入すると`rail c`で動作確認できます。
```
$ rails c
irb(main):001> results = Geocoder.search("Paris")
=> 
[#<Geocoder::Result:
...
irb(main):002> results.first.coordinates
=> [48.8534951, 2.3483915]
```
### 3. Google Maps API(Geocoding API)を参照先に設定
gem geocogerはさまざまなジオコーディングサービスを参照することが可能です。住所の場合デフォルトの設定だと`:nominatim`となっていますが、こちらの記事（[参考](https://qiita.com/Sasaki_51660skbb/items/629a7f78e118e3bac1bc#6%E4%BD%8F%E6%89%80%E3%82%92%E7%99%BB%E9%8C%B2%E3%81%A7%E3%81%8D%E3%81%A6%E3%82%8B%E3%81%AE%E3%81%ABmap%E3%81%8C%E8%A1%A8%E7%A4%BA%E3%81%95%E3%82%8C%E3%81%AA%E3%81%84%E6%99%82)）のようにデフォルトのままだと、住所から正しい緯度経度が検索できないケースもあるようです。今回はGoogle Maps APIのGeocoding APIを参照先にする設定をしていきます。
参照できるAPIの一覧は下記のリンクに記載があります。

https://github.com/alexreisner/geocoder/blob/master/README_API_GUIDE.md
#### 1. APIキーの取得・導入
Google Cloud Platformでプロジェクトの作成、Geocoding APIの有効化、APIキーの取得といった流れになります。
また、APIキーは外部に公開してはならないため、あらかじめ環境変数として設定します。
下記記事の最初から「実装>1.APIキーを環境変数化」までの流れを参考にしてみてください。

https://qiita.com/mattan5271/items/196fa1941da2152b6d5d
#### 2. Geocogerでの設定
続いて、アプリでGeocoding APIを参照できるようにgem geocoderの設定をしていきます。まず、設定のためのファイルを作成します。
```
$ rails generate geocoder:config
```
設定ファイルでGeocoding APIを参照できるように設定します。（必要な箇所のみ抜粋しています。
```ruby:config/initializers/geocoder.rb
Geocoder.configure(
   lookup: :google,
   use_https: true,
   api_key: ENV['GOOGLE_MAP_API'],
)
```
参照できるAPIの中に他にもGoogleのAPIを利用したものがありますが、Geocoding APIの場合は`lookup: :google`という記載方法になります（[参考](https://github.com/alexreisner/geocoder/blob/master/README_API_GUIDE.md#google-google)）。
## 今回詰まった点について
今回、下記のように実装しました。（必要な箇所のみ抜粋してあります）
```ruby:app/views/posts/_form.html.erb
<%= form_with model: post do |f| %>
    #...
    <%= f.label :address, '住所', class: 'label w-full md:w-1/2 mx-auto' %>
    <%= f.text_field :address, class: 'input input-borderd input-accent form-control mb-6 w-full md:w-1/2 mx-auto' %>
    #...
    <%= f.submit '投稿', class: 'btn btn-primary w-full my-6' %>
    #...
<% end %>
```
```ruby:app/controllers/posts_controller.rb
  def create
    @post = current_user.posts.build(post_params)
    if @post.save
      redirect_to posts_path, success: '投稿に成功しました'
    else
      flash.now[:alert] = '投稿に失敗しました'
      render :new, status: :unprocessable_entity
    end
  end
```
```ruby:app/models/post.rb
validates :address, presence: true
validates :latitude, presence: true
validates :longitude, presence: true
geocoded_by :address
after_validation :geocode, if: :address_changed?
```
必要な情報をフォームに入力し投稿しようとすると「投稿に失敗しました」とフラッシュメッセージが表示されました。
## デバックで原因を調べる
コントローラに`pp`を差し込んでプリントデバックをしてみました。
```ruby
  def create
    @post = current_user.posts.build(post_params)
    pp @post
    #=> latitude: nil,
    #=> longitude: nil,
    if @post.save
      redirect_to posts_path, success: '投稿に成功しました'
    else
      pp @post
    #=> latitude: 値入ってる,
    #=> longitude: 値入ってる,
      flash.now[:alert] = '投稿に失敗しました'
      render :new, status: :unprocessable_entity
    end
  end
```
上記に加えて、`pp @post.errors.full_messages.join(", ")`を試した際も、`ja.activerecord.errors.models.post.attributes.latitude.blank`や`ja.activerecord.errors.models.post.attributes.longitude.blank`というようにログが出て、latitudeとlongitudeの値が抜けてしまっているのが原因ではないかと考えました。
## 原因
今回の原因は、Postモデルでのバリデーションでした。
```ruby:app/models/post.rb
validates :address, presence: true
validates :latitude, presence: true
validates :longitude, presence: true
# geocodingについての設定
geocoded_by :address
after_validation :geocode, if: :address_changed?
```
今回のように実装をすると、`:geocode`の呼び出しが[after_validation](https://api.rubyonrails.org/v7.1/classes/ActiveModel/Validations/Callbacks/ClassMethods.html#method-i-after_validation)すなわち、バリデーションの直後に呼び出されます。すなわち、`:latitude`と`:longitude`が`presence: true`であることを確認してから、住所から緯度と経度の取得を行うという流れになってしまっていました。

その後、バリデーションをクリアできないため`save`はできないものの、`:geocode`は呼び出されるため、`else`以下で`:latitude`と`:longitude`の値が確認できたのだと思われます。
## 解決方法
上記の解決方法として2つ考えました。
### 1. longitudeとlongitudeにバリデーションを設定しない

```ruby:app/models/post.rb
validates :address, presence: true
# geocodingについての設定
geocoded_by :address
after_validation :geocode, if: :address_changed?
```
そもそも入力時にはlongitudeとlongitudeの値はないのだからバリデーションを外してしまう方法です。

ただ、この方法で仮に存在し得ない住所を入力してジオコーディングで緯度経度が取得できなかった場合どうなるのか気になったので確かめてみました。

```
web-1  | 14:01:20 web.1  |  id: nil,
web-1  | 14:01:20 web.1  |  restaurant_name: "a",
web-1  | 14:01:20 web.1  |  address: " a",
web-1  | 14:01:20 web.1  |  latitude: nil,
web-1  | 14:01:20 web.1  |  longitude: nil,
web-1  | 14:01:20 web.1  |  body: "umai\n",
web-1  | 14:01:20 web.1  |  amount: nil,
web-1  | 14:01:20 web.1  |  user_id: 11,
web-1  | 14:01:20 web.1  |  created_at: nil,
web-1  | 14:01:20 web.1  |  updated_at: nil,
web-1  | 14:01:20 web.1  |  genre: "japanese_food">
web-1  | 14:01:20 web.1  |   TRANSACTION (0.6ms)  BEGIN
web-1  | 14:01:20 web.1  |   ↳ app/controllers/posts_controller.rb:14:in `create'
web-1  | 14:01:20 web.1  |   Post Create (8.4ms)  INSERT INTO `posts` (`restaurant_name`, `address`, `latitude`, `longitude`, `body`, `amount`, `user_id`, `created_at`, `updated_at`, `genre`) VALUES ('a', ' a', NULL, NULL, 'umai\n', NULL, 11, '2024-07-19 05:01:20.235673', '2024-07-19 05:01:20.235673', 0)
web-1  | 14:01:20 web.1  |   ↳ app/controllers/posts_controller.rb:14:in `create'
web-1  | 14:01:20 web.1  |   TRANSACTION (0.3ms)  ROLLBACK
web-1  | 14:01:20 web.1  |   ↳ app/controllers/posts_controller.rb:14:in `create'
web-1  | 14:01:20 web.1  | Completed 500 Internal Server Error in 192ms (ActiveRecord: 9.8ms | Allocations: 6186)
web-1  | 14:01:20 web.1  | 
web-1  | 14:01:20 web.1  | 
web-1  | 14:01:20 web.1  |   
web-1  | 14:01:20 web.1  | ActiveRecord::NotNullViolation (Mysql2::Error: Column 'latitude' cannot be null):
```

どうやらDB上で`Mysql2::Error: Column 'latitude' cannot be null`というエラーが発生するようです。ただ、今回は住所をGoogle Maps APIのPlaces APIを使用したオートコンプリートを使うことから、通常の使用をしていれば大きな問題にならないと考えこちらで実装予定です。

### 2. `:geocode`を`before_validation`にする
```ruby:app/models/post.rb
validates :address, presence: true
validates :latitude, presence: true
validates :longitude, presence: true
geocoded_by :address
before_validation :geocode, if: :address_changed?
```
続いてバリデーションの実行前に`:geocode`を実行するという案です。こうすることで、longitudeとlongitudeのバリデーションを保ちつつ、ジオコーディングできます。

しかしながら、ジオコーディングした後にバリデーションをするため、他の入力必須項目で未入力があった場合でも住所が入力されていれば緯度・経度の取得を行います。すなわち、DB保存できないタイミングでもAPIにリクエストをしてしまい、特に費用のかかる外部APIを使用している場合適切ではないと考えました。
## まとめ
gem geocoderとGoogle Maps APIのGeocoding APIを使用して、住所から緯度・経度の情報を取得する流れとバリデーションの設定で詰まった箇所についてまとめていきました。
DBで`not: null`制約をしたものはモデルにおいても`presence: true`にするべしとこれまであまり考えずに実装していたため勉強になりました。
何かご指摘等あれば教えていただけると幸いです。ここまで読んでいただきありがとうございました！！