geocoder gemのnearを使ってJOINでの検索をする時気をつけること

railsでgemのgeocoderを使ってJOINしたときにJOIN先テーブルのSELECTがついてしまい、JOIN先のデータで上書きされてしまうというのでハマったのでメモしておきます。

結論としてはgeocoderのオプションでSELECTを指定する必要があったということでした。

定義

Groupは1対多でShopのレコードを持つようにしています。

groupモデル

class Group < ApplicationRecord
  has_many :shops
end

データ

id name
1 セブンイレブン

Shopモデル

Shopモデルにはnearメソッドを使用できるように設定しておきます。

class Shop < ApplicationRecord
  geocoded_by latitude: :latitude, longitude: :longitude
end

データ

id name group_id latitude longitude
1 セブンイレブン 那覇松山1丁目店 1 26.2177288 127.6799351
2 セブンイレブン 那覇松山2丁目店 1 26.2186288 127.6804286
3 セブンイレブン 那覇西2丁目店 1 26.2130606 127.6705822

実行

groupsテーブルにshopsテーブルをJOINして実行してみます(この場合はJOINを使わなくてもshopsテーブルを基準に条件を設定すれば必要な検索はできると思います)

shop = Shop.find(1)
Group
  .where(id: 1)
  .joins(:shops)
  .merge(Shop.near([shop.latitude, shop.longitude], 0.5, units: :km))

すると、GroupのidとnameがShopのidとnameで書き換わってしまっています。

[#<Group:0x00007ff19461d338
  id: 1,
  name: "セブンイレブン 那覇松山1丁目店">,
 #<Group:0x00007ff19461d270
  id: 2,
  name: "セブンイレブン 那覇松山2丁目店">]

クエリを確認してみると

SELECT
  shops.*,
  6371.0 * 2 * ASIN(SQRT(POWER(SIN((26.2177288 - shops.latitude) * PI() / 180 / 2), 2) + COS(26.2177288 * PI() / 180) * COS(shops.latitude * PI() / 180) * POWER(SIN((127.6799351 - shops.longitude) * PI() / 180 / 2), 2))) AS distance,
  MOD(CAST((ATAN2(((shops.longitude - 127.6799351) / 57.2957795),((shops.latitude - 26.2177288) / 57.2957795)) * 57.2957795) + 360 AS decimal), 360) AS bearing
FROM
  `groups`
  INNER JOIN
    `shops`
  ON  `shops`.`group_id` = `groups`.`id`
WHERE
  `groups`.`id` = 1
AND (
    shops.latitude BETWEEN 26.213232191970405 AND 26.222225408029594
  AND shops.longitude BETWEEN 127.67492283916334 AND 127.68494736083665
  AND (6371.0 * 2 * ASIN(SQRT(POWER(SIN((26.2177288 - shops.latitude) * PI() / 180 / 2), 2) + COS(26.2177288 * PI() / 180) * COS(shops.latitude * PI() / 180) * POWER(SIN((127.6799351 - shops.longitude) * PI() / 180 / 2), 2)))) BETWEEN 0.0 AND 0.5
  )
ORDER BY
  distance ASC
LIMIT 11

のようになっており、shops.*がついてしまっているのが原因でした。

このSELECTはオプションで設定可能で、select: 'shops.id AS shop_id, shops.name AS shop_name'の様に引数を渡せば設定可能です。

SELECTを設定する

Group
  .where(id: 1)
  .joins(:shops)
  .select('groups.id, groups.name')
  .merge(Shop.near([shop.latitude, shop.longitude], 0.5, units: :km, select: 'shops.id AS shop_id, shops.name AS shop_name'))

SQL

SELECT
  groups.id,
  groups.name,
  shops.id AS shop_id,
  shops.name AS shop_name,
  6371.0 * 2 * ASIN(SQRT(POWER(SIN((26.2177288 - shops.latitude) * PI() / 180 / 2), 2) + COS(26.2177288 * PI() / 180) * COS(shops.latitude * PI() / 180) * POWER(SIN((127.6799351 - shops.longitude) * PI() / 180 / 2), 2))) AS distance,
  MOD(CAST((ATAN2(((shops.longitude - 127.6799351) / 57.2957795),((shops.latitude - 26.2177288) / 57.2957795)) * 57.2957795) + 360 AS decimal), 360) AS bearing
FROM
  `groups`
  INNER JOIN
    `shops`
  ON  `shops`.`group_id` = `groups`.`id`
WHERE
  `groups`.`id` = 1
AND (
    shops.latitude BETWEEN 26.213232191970405 AND 26.222225408029594
  AND shops.longitude BETWEEN 127.67492283916334 AND 127.68494736083665
  AND (6371.0 * 2 * ASIN(SQRT(POWER(SIN((26.2177288 - shops.latitude) * PI() / 180 / 2), 2) + COS(26.2177288 * PI() / 180) * COS(shops.latitude * PI() / 180) * POWER(SIN((127.6799351 - shops.longitude) * PI() / 180 / 2), 2)))) BETWEEN 0.0 AND 0.5
  )
ORDER BY
  distance ASC
LIMIT 11

結果

これで上書きされないようになりました。ちなみにShopのデータはshop_idshop_nameとかでアクセスすれば取得できます。

[#<Group:0x00007ff192776790 id: 1, name: "セブンイレブン">,
 #<Group:0x00007ff1927766a0 id: 1, name: "セブンイレブン">]