basho 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 60bd164ce175bcaf79c4039521edc6a51712fc979edf3ae9a02c10841f624949
4
- data.tar.gz: c00ec624a6523026bb2d1f219d8d301cdf1bd7488bc542d31b644e264989201d
3
+ metadata.gz: 0d571071cfeea78be4b3afdb28200668b434c582a17382a170e25db60c9e5939
4
+ data.tar.gz: 61a142f2363cce678572ca3815c5bcf3754a8722afa6437321c5999beab8d454
5
5
  SHA512:
6
- metadata.gz: '00694bd8ed6fd2e1f7a385aa1051cdaba6cb89b8636c65793a6da5f43f586ce9c7f52b7ed6caf1e1947ea5f1185e74509b515b45d5df6c479a6a4eb30efcf7d3'
7
- data.tar.gz: be33e1e0f6c0fa08284e84e7d70c5b1cb3b5d84bf6745cf7b88048ffed96dad8b256e62bc4a483d120162250f9b9ce4fbec2ce0704db4fd26ed1764dfe9f0a06
6
+ metadata.gz: 3ba4b016103fd8d4564080729dd82c20d0da5bdaa01ebc41a05438b94066abb9f7d38d8820b344ce622e426d735623d3f44972d01a4957def4fb3e3af2e8d639
7
+ data.tar.gz: 590d049ff6bde79407682e06806232841b3d40249588c09dd128cc6cf1b8f06afac66e2bbbe80473142ab6aff4178cf5bef81de6db2b69d7c980f4d4a57d273b
data/CHANGELOG.md CHANGED
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.5.0] - 2026-02-11
9
+
10
+ ### Added
11
+
12
+ - `with_basho` スコープ — city・prefectureをeager loadしN+1クエリを防止(メモリモードではno-op)
13
+ - `has_one :basho_prefecture` — prefecture直接プリロード対応(DBモード)
14
+ - `Basho::DB::City` に廃止・合併管理機能(`deprecated_at`, `successor_code`, `#current`, `.active`, `.deprecated`)
15
+ - `Basho::City`(メモリモード)にも同等の廃止・合併API(`#deprecated?`, `#active?`, `#successor`, `#current`)
16
+ - `Basho::DB.seed_fresh?` — DBデータの鮮度チェック(Rails起動時に自動警告)
17
+ - `rails generate basho:upgrade_deprecation` — 既存テーブルに廃止管理カラムを追加するマイグレーションジェネレータ
18
+ - `MAX_SUCCESSOR_DEPTH` — 合併チェーン探索の深度制限(ループ検出に加えた安全弁)
19
+
20
+ ### Changed
21
+
22
+ - `Basho::DB.seed!` を `delete_all` + `insert_all!` から `upsert_all` に変更(手動設定の `successor_code` / `deprecated_at` を保持)
23
+ - gemデータから消えた市区町村を物理削除ではなく論理削除(`deprecated_at` を設定)に変更
24
+ - `basho` マクロのリファクタリング(DB/メモリモードの分離、メソッド分割)
25
+
8
26
  ## [0.4.1] - 2026-02-11
9
27
 
10
28
  ### Fixed
@@ -103,6 +121,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
103
121
  - GitHub Actions CI(Ruby 3.2/3.3/3.4)
104
122
  - 月次データ自動更新ワークフロー
105
123
 
124
+ [0.5.0]: https://github.com/wagai/basho/releases/tag/v0.5.0
106
125
  [0.4.1]: https://github.com/wagai/basho/releases/tag/v0.4.1
107
126
  [0.4.0]: https://github.com/wagai/basho/releases/tag/v0.4.0
108
127
  [0.3.0]: https://github.com/wagai/basho/releases/tag/v0.3.0
data/README.ja.md CHANGED
@@ -197,13 +197,28 @@ shop.prefecture # => Basho::Prefecture
197
197
  shop.full_address # => "東京都千代田区"
198
198
  ```
199
199
 
200
- `basho :column`は3つのインスタンスメソッドを定義します:
200
+ `basho :column`は3つのインスタンスメソッドとスコープを定義します:
201
201
 
202
202
  | メソッド | 戻り値 |
203
203
  |---------|--------|
204
204
  | `city` | カラム値で検索した`Basho::City` |
205
205
  | `prefecture` | `city.prefecture`経由の`Basho::Prefecture` |
206
206
  | `full_address` | `"#{prefecture.name}#{city.name}"` または `nil` |
207
+ | `with_basho` | city・prefectureをプリロードするスコープ(N+1防止) |
208
+
209
+ #### N+1防止
210
+
211
+ 複数レコードで`city`や`prefecture`にアクセスする場合は`with_basho`スコープを使います:
212
+
213
+ ```ruby
214
+ # なし: N+1クエリ(1 + N×2)
215
+ Shop.all.each { |s| s.full_address }
216
+
217
+ # あり: 合計3クエリ
218
+ Shop.with_basho.each { |s| s.full_address }
219
+ ```
220
+
221
+ `with_basho`はメモリモード・DBモード両方で動きます。メモリモードではno-op、DBモードではアソシエーションをeager loadします。DB切り替え前から書いておけば、切り替え後にコード変更は不要です。
207
222
 
208
223
  ### 郵便番号から住所文字列を取得
209
224
 
data/README.md CHANGED
@@ -197,13 +197,28 @@ shop.prefecture # => Basho::Prefecture
197
197
  shop.full_address # => "東京都千代田区"
198
198
  ```
199
199
 
200
- `basho :column` defines three instance methods:
200
+ `basho :column` defines three instance methods and a scope:
201
201
 
202
202
  | Method | Return value |
203
203
  |--------|-------------|
204
204
  | `city` | `Basho::City` found by the column value |
205
205
  | `prefecture` | `Basho::Prefecture` via `city.prefecture` |
206
206
  | `full_address` | `"#{prefecture.name}#{city.name}"` or `nil` |
207
+ | `with_basho` | Scope that preloads city and prefecture (N+1 prevention) |
208
+
209
+ #### N+1 Prevention
210
+
211
+ Use the `with_basho` scope when loading multiple records that access `city` or `prefecture`:
212
+
213
+ ```ruby
214
+ # Without: N+1 queries (1 + N×2)
215
+ Shop.all.each { |s| s.full_address }
216
+
217
+ # With: 3 queries total
218
+ Shop.with_basho.each { |s| s.full_address }
219
+ ```
220
+
221
+ `with_basho` works in both memory and DB mode. In memory mode it is a no-op; in DB mode it eager-loads the associations. This means you can add it before switching to DB mode -- no code changes needed later.
207
222
 
208
223
  ### Get an address string from a postal code
209
224
 
@@ -0,0 +1 @@
1
+ []
@@ -15,20 +15,19 @@ module Basho
15
15
  # basho_postal :postal_code, prefecture: :pref_name, city: :city_name
16
16
  # end
17
17
  module Base
18
- # 自治体コードカラムから +city+, +prefecture+, +full_address+ メソッドを定義する。
18
+ # 自治体コードカラムから +city+, +prefecture+, +full_address+ メソッドと
19
+ # +with_basho+ スコープを定義する。
20
+ #
21
+ # DBモードでは +belongs_to :basho_city+ と +has_one :basho_prefecture+ を追加し、
22
+ # +with_basho+ スコープでN+1クエリを防止する。
23
+ # メモリモードでは +with_basho+ はno-op(+all+)となる。
19
24
  #
20
25
  # @param column [Symbol, String] 6桁自治体コードを格納するカラム名
21
26
  # @return [void]
22
27
  def basho(column)
23
28
  column_name = column.to_s
24
-
25
- define_method(:city) { (c = send(column_name)) && Basho::City.find(c) }
26
- define_method(:prefecture) { city&.prefecture }
27
- define_method(:full_address) do
28
- pref = prefecture
29
- cty = city
30
- "#{pref.name}#{cty.name}" if pref && cty
31
- end
29
+ Basho.db? ? basho_db_mode(column_name) : basho_memory_mode(column_name)
30
+ basho_define_full_address
32
31
  end
33
32
 
34
33
  # 郵便番号カラムから +postal_address+ メソッドを定義する。
@@ -57,6 +56,57 @@ module Basho
57
56
 
58
57
  PostalAutoResolve.install(self, column_name, mappings) if mappings.any?
59
58
  end
59
+
60
+ private
61
+
62
+ # @private DBモードのアソシエーションとスコープを定義する。
63
+ def basho_db_mode(column_name)
64
+ basho_db_associations(column_name)
65
+ scope :with_basho, -> { includes(basho_city: :prefecture) }
66
+ basho_db_methods
67
+ end
68
+
69
+ # @private DBモードのアソシエーションを定義する。
70
+ def basho_db_associations(column_name)
71
+ belongs_to :basho_city,
72
+ class_name: "Basho::DB::City",
73
+ foreign_key: column_name,
74
+ primary_key: "code",
75
+ optional: true
76
+
77
+ has_one :basho_prefecture,
78
+ through: :basho_city,
79
+ source: :prefecture
80
+ end
81
+
82
+ # @private DBモードのインスタンスメソッドを定義する。
83
+ def basho_db_methods
84
+ define_method(:city) { basho_city }
85
+ define_method(:prefecture) do
86
+ if association(:basho_prefecture).loaded?
87
+ basho_prefecture
88
+ else
89
+ basho_city&.prefecture
90
+ end
91
+ end
92
+ end
93
+
94
+ # @private メモリモードのスコープ・メソッドを定義する。
95
+ def basho_memory_mode(column_name)
96
+ scope(:with_basho, -> { all }) if respond_to?(:scope)
97
+
98
+ define_method(:city) { (c = send(column_name)) && Basho::City.find(c) }
99
+ define_method(:prefecture) { city&.prefecture }
100
+ end
101
+
102
+ # @private full_addressメソッドを定義する。
103
+ def basho_define_full_address
104
+ define_method(:full_address) do
105
+ pref = prefecture
106
+ cty = city
107
+ "#{pref.name}#{cty.name}" if pref && cty
108
+ end
109
+ end
60
110
  end
61
111
  end
62
112
  end
data/lib/basho/city.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Basho
4
+ MAX_SUCCESSOR_DEPTH = 10
4
5
  # 市区町村を表すイミュータブルなデータクラス。
5
6
  #
6
7
  # DBバックエンドが有効な場合、クラスメソッドは自動的に {DB::City} 経由で検索する。
@@ -17,8 +18,13 @@ module Basho
17
18
  # @return [String, nil] 郡名(例: "島尻郡")。郡に属する町村のみ設定
18
19
  # @!attribute [r] capital
19
20
  # @return [Boolean] 県庁所在地フラグ
20
- City = ::Data.define(:code, :prefecture_code, :name, :name_kana, :district, :capital) do
21
- def initialize(district: nil, capital: false, **)
21
+ # @!attribute [r] deprecated_at
22
+ # @return [String, nil] 廃止日(例: "2025-04-01")。現行自治体は +nil+
23
+ # @!attribute [r] successor_code
24
+ # @return [String, nil] 合併先の6桁自治体コード。合併先がない場合は +nil+
25
+ City = ::Data.define(:code, :prefecture_code, :name, :name_kana, :district, :capital,
26
+ :deprecated_at, :successor_code) do
27
+ def initialize(district: nil, capital: false, deprecated_at: nil, successor_code: nil, **)
22
28
  super
23
29
  end
24
30
 
@@ -34,6 +40,43 @@ module Basho
34
40
  district ? "#{district}#{name}" : name
35
41
  end
36
42
 
43
+ # 廃止済みかどうかを返す。
44
+ #
45
+ # @return [Boolean]
46
+ def deprecated? = !deprecated_at.nil?
47
+
48
+ # 現行(未廃止)かどうかを返す。
49
+ #
50
+ # @return [Boolean]
51
+ def active? = deprecated_at.nil?
52
+
53
+ # 合併先の市区町村を返す。
54
+ #
55
+ # @return [City, nil]
56
+ def successor
57
+ City.find(successor_code) if successor_code
58
+ end
59
+
60
+ # 合併チェーンをたどり、現行の市区町村を返す。
61
+ # アクティブ市区町村は即座に自身を返す。ループ検出・深度制限付き。
62
+ #
63
+ # @return [City]
64
+ def current
65
+ return self unless successor_code
66
+
67
+ city = self
68
+ seen = Set.new
69
+ while city.successor_code && seen.add?(city.successor_code)
70
+ break if seen.size > MAX_SUCCESSOR_DEPTH
71
+
72
+ next_city = City.find(city.successor_code)
73
+ break unless next_city
74
+
75
+ city = next_city
76
+ end
77
+ city
78
+ end
79
+
37
80
  # 所属する都道府県を返す。
38
81
  #
39
82
  # @return [Prefecture, DB::Prefecture]
@@ -51,7 +94,8 @@ module Basho
51
94
  return DB::City.find_by(code: code) if Basho.db?
52
95
 
53
96
  pref_code = code[0..1].to_i
54
- where(prefecture_code: pref_code).find { |city| city.code == code }
97
+ where(prefecture_code: pref_code).find { |city| city.code == code } ||
98
+ find_deprecated(code)
55
99
  end
56
100
 
57
101
  # 都道府県コードで市区町村を絞り込む。
@@ -71,6 +115,12 @@ module Basho
71
115
  def valid_code?(code)
72
116
  CodeValidator.valid?(code)
73
117
  end
118
+
119
+ private
120
+
121
+ def find_deprecated(code)
122
+ Data::Loader.deprecated_city(code)&.then { |data| new(**data) }
123
+ end
74
124
  end
75
125
  end
76
126
  end
@@ -29,6 +29,21 @@ module Basho
29
29
  cities_cache[prefecture_code] ||= load_json("cities/#{format("%02d", prefecture_code)}.json")
30
30
  end
31
31
 
32
+ # 廃止された市区町村データを返す。
33
+ #
34
+ # @return [Array<Hash>]
35
+ def deprecated_cities
36
+ @deprecated_cities ||= load_json("deprecated_cities.json")
37
+ end
38
+
39
+ # 廃止コードで1件検索する(ハッシュインデックスによる O(1) ルックアップ)。
40
+ #
41
+ # @param code [String] 6桁自治体コード
42
+ # @return [Hash, nil]
43
+ def deprecated_city(code)
44
+ deprecated_cities_by_code[code]
45
+ end
46
+
32
47
  # 指定したプレフィックスの郵便番号データを返す。
33
48
  #
34
49
  # @param prefix [String] 3桁プレフィックス(例: "154")
@@ -54,6 +69,10 @@ module Basho
54
69
  @postal_cache ||= {}
55
70
  end
56
71
 
72
+ def deprecated_cities_by_code
73
+ @deprecated_cities_by_code ||= deprecated_cities.to_h { |c| [c[:code], c] }
74
+ end
75
+
57
76
  def load_json(relative_path)
58
77
  path = File.join(DATA_DIR, relative_path)
59
78
  return [] unless path.start_with?("#{DATA_DIR}/") && File.exist?(path)
data/lib/basho/db/city.rb CHANGED
@@ -13,6 +13,35 @@ module Basho
13
13
  foreign_key: :prefecture_code,
14
14
  inverse_of: :cities
15
15
 
16
+ belongs_to :successor,
17
+ class_name: "Basho::DB::City",
18
+ foreign_key: :successor_code,
19
+ primary_key: :code,
20
+ optional: true
21
+
22
+ scope :active, -> { where(deprecated_at: nil) }
23
+ scope :deprecated, -> { where.not(deprecated_at: nil) }
24
+
25
+ def deprecated? = deprecated_at.present?
26
+ def active? = deprecated_at.nil?
27
+
28
+ # 合併チェーンをたどって現行の自治体を返す(ループ検出・深度制限付き)。
29
+ #
30
+ # @return [Basho::DB::City]
31
+ def current
32
+ city = self
33
+ seen = Set.new
34
+ while city.successor_code.present? && seen.add?(city.successor_code)
35
+ break if seen.size > MAX_SUCCESSOR_DEPTH
36
+
37
+ next_city = city.successor
38
+ break unless next_city
39
+
40
+ city = next_city
41
+ end
42
+ city
43
+ end
44
+
16
45
  # 郡名付きの正式名を返す。
17
46
  #
18
47
  # @return [String]
data/lib/basho/db.rb CHANGED
@@ -9,6 +9,7 @@ module Basho
9
9
  # +basho_prefectures+ / +basho_cities+ テーブルへのアクセスとシードを提供する。
10
10
  module DB
11
11
  # JSONデータをDBに一括投入する。冪等(何度実行しても同じ結果)。
12
+ # upsert_allで既存レコードを保持しつつ更新し、gemデータから消えた市区町村は論理削除する。
12
13
  #
13
14
  # @return [Hash{Symbol => Integer}] 投入件数(+:prefectures+, +:cities+)
14
15
  def self.seed!
@@ -16,16 +17,41 @@ module Basho
16
17
  cities = city_rows
17
18
 
18
19
  ::ActiveRecord::Base.transaction do
19
- City.delete_all
20
- Prefecture.delete_all
21
-
22
- Prefecture.insert_all!(prefs)
23
- City.insert_all!(cities)
20
+ Prefecture.upsert_all(prefs, unique_by: :code)
21
+ upsert_cities(cities)
22
+ deprecate_removed_cities(cities)
24
23
  end
25
24
 
26
25
  { prefectures: prefs.size, cities: cities.size }
27
26
  end
28
27
 
28
+ # DBのアクティブな市区町村件数がgemの同梱データと一致するか判定する。
29
+ # 市区町村の合併・分割で件数が変わるため、不一致はシード更新が必要なサイン。
30
+ #
31
+ # @return [Boolean]
32
+ def self.seed_fresh?
33
+ expected = (1..47).sum { |code| Data::Loader.cities(code).size }
34
+ City.active.count == expected
35
+ rescue ::ActiveRecord::ActiveRecordError
36
+ false
37
+ end
38
+
39
+ def self.upsert_cities(cities)
40
+ City.upsert_all(
41
+ cities,
42
+ unique_by: :code,
43
+ update_only: %i[prefecture_code name name_kana district capital]
44
+ )
45
+ end
46
+ private_class_method :upsert_cities
47
+
48
+ def self.deprecate_removed_cities(cities)
49
+ gem_codes = cities.to_set { |c| c[:code] }
50
+ stale_codes = City.where(deprecated_at: nil).pluck(:code).reject { |c| gem_codes.include?(c) }
51
+ City.where(code: stale_codes).update_all(deprecated_at: Time.current) if stale_codes.any?
52
+ end
53
+ private_class_method :deprecate_removed_cities
54
+
29
55
  def self.prefecture_rows
30
56
  Data::Loader.prefectures.map do |pref|
31
57
  pref.except(:type).merge(prefecture_type: pref[:type])
data/lib/basho/engine.rb CHANGED
@@ -19,6 +19,20 @@ module Basho
19
19
  app.config.assets.paths << root.join("app/assets/javascripts") if app.config.respond_to?(:assets)
20
20
  end
21
21
 
22
+ initializer "basho.seed_freshness_check" do
23
+ config.after_initialize do
24
+ next unless Basho.db?
25
+ next if Basho::DB.seed_fresh?
26
+
27
+ Rails.logger.warn(
28
+ "[basho] DBのアクティブな市区町村件数がgemのデータと一致しません。" \
29
+ "`rails basho:seed` を実行してください"
30
+ )
31
+ rescue ::ActiveRecord::ActiveRecordError
32
+ # DB未接続・マイグレーション前は無視
33
+ end
34
+ end
35
+
22
36
  rake_tasks do
23
37
  load File.expand_path("../tasks/basho.rake", __dir__)
24
38
  end
data/lib/basho/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Basho
4
4
  # @return [String] 現在のgemバージョン
5
- VERSION = "0.4.1"
5
+ VERSION = "0.5.0"
6
6
  end
@@ -7,6 +7,8 @@ class CreateBashoCities < ActiveRecord::Migration[<%= ActiveRecord::Migration.cu
7
7
  t.string :name_kana, null: false
8
8
  t.string :district
9
9
  t.boolean :capital, null: false, default: false
10
+ t.datetime :deprecated_at
11
+ t.string :successor_code, limit: 6
10
12
  end
11
13
 
12
14
  add_index :basho_cities, :prefecture_code
@@ -0,0 +1,6 @@
1
+ class AddDeprecationToBashoCities < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ add_column :basho_cities, :deprecated_at, :datetime
4
+ add_column :basho_cities, :successor_code, :string, limit: 6
5
+ end
6
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module Basho
7
+ module Generators
8
+ # 既存の +basho_cities+ テーブルに +deprecated_at+ / +successor_code+ を追加するマイグレーションジェネレータ。
9
+ #
10
+ # @example
11
+ # rails generate basho:upgrade_deprecation
12
+ class UpgradeDeprecationGenerator < Rails::Generators::Base
13
+ include ::ActiveRecord::Generators::Migration
14
+
15
+ source_root File.expand_path("templates", __dir__)
16
+
17
+ desc "basho_cities テーブルに deprecated_at / successor_code カラムを追加"
18
+
19
+ def create_migration_file
20
+ migration_template(
21
+ "add_deprecation_to_basho_cities.rb.erb",
22
+ "db/migrate/add_deprecation_to_basho_cities.rb"
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: basho
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hirotaka Wagai
@@ -76,6 +76,7 @@ files:
76
76
  - data/cities/45.json
77
77
  - data/cities/46.json
78
78
  - data/cities/47.json
79
+ - data/deprecated_cities.json
79
80
  - data/postal_codes/001.json
80
81
  - data/postal_codes/002.json
81
82
  - data/postal_codes/003.json
@@ -1042,6 +1043,8 @@ files:
1042
1043
  - lib/generators/basho/install_tables/install_tables_generator.rb
1043
1044
  - lib/generators/basho/install_tables/templates/create_basho_cities.rb.erb
1044
1045
  - lib/generators/basho/install_tables/templates/create_basho_prefectures.rb.erb
1046
+ - lib/generators/basho/upgrade_deprecation/templates/add_deprecation_to_basho_cities.rb.erb
1047
+ - lib/generators/basho/upgrade_deprecation/upgrade_deprecation_generator.rb
1045
1048
  - lib/tasks/basho.rake
1046
1049
  - sig/basho.rbs
1047
1050
  homepage: https://github.com/wagai/basho
@@ -1066,7 +1069,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
1066
1069
  - !ruby/object:Gem::Version
1067
1070
  version: '0'
1068
1071
  requirements: []
1069
- rubygems_version: 4.0.4
1072
+ rubygems_version: 3.6.9
1070
1073
  specification_version: 4
1071
1074
  summary: Japanese address data (prefectures, cities, postal codes, regions) in a single
1072
1075
  gem