active_record_in_time_scope 0.1.7

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.
data/docs/ja/index.md ADDED
@@ -0,0 +1,192 @@
1
+ # InTimeScope
2
+
3
+ Railsでこんなコードを毎回書いていませんか?
4
+
5
+ ```ruby
6
+ # Before
7
+ Event.where("start_at <= ? AND (end_at IS NULL OR end_at > ?)", Time.current, Time.current)
8
+
9
+ # After
10
+ class Event < ActiveRecord::Base
11
+ in_time_scope
12
+ end
13
+
14
+ Event.in_time
15
+ ```
16
+
17
+ これだけです。DSL1行で、モデルに生SQLを書く必要がなくなります。
18
+
19
+ ## このGemを使う理由
20
+
21
+ このGemの目的:
22
+
23
+ - **時間範囲ロジックの一貫性を保つ** - コードベース全体で統一
24
+ - **コピペSQLを回避** - 間違えやすいSQLの繰り返しを防止
25
+ - **時間をファーストクラスのドメイン概念に** - `in_time_published`のような名前付きスコープ
26
+ - **NULL許可を自動検出** - スキーマから最適化されたクエリを生成
27
+
28
+ ## 推奨される用途
29
+
30
+ - 有効期限を持つRailsアプリケーション
31
+ - `start_at` / `end_at`カラムを持つモデル
32
+ - 散在する`where`句なしで一貫した時間ロジックを求めるチーム
33
+
34
+ ## インストール
35
+
36
+ ```bash
37
+ bundle add in_time_scope
38
+ ```
39
+
40
+ ## クイックスタート
41
+
42
+ ```ruby
43
+ class Event < ActiveRecord::Base
44
+ in_time_scope
45
+ end
46
+
47
+ # クラススコープ
48
+ Event.in_time # 現在有効なレコード
49
+ Event.in_time(Time.parse("2024-06-01")) # 特定時刻に有効なレコード
50
+
51
+ # インスタンスメソッド
52
+ event.in_time? # このレコードは現在有効?
53
+ event.in_time?(some_time) # その時刻に有効だった?
54
+ ```
55
+
56
+ ## 機能
57
+
58
+ ### 自動最適化されたSQL
59
+
60
+ GemがスキーマをベースにSQLを読み取り、適切なSQLを生成します:
61
+
62
+ ```ruby
63
+ # NULL許可カラム → NULL対応クエリ
64
+ WHERE (start_at IS NULL OR start_at <= ?) AND (end_at IS NULL OR end_at > ?)
65
+
66
+ # NOT NULLカラム → シンプルなクエリ
67
+ WHERE start_at <= ? AND end_at > ?
68
+ ```
69
+
70
+ ### 名前付きスコープ
71
+
72
+ モデルごとに複数の時間範囲:
73
+
74
+ ```ruby
75
+ class Article < ActiveRecord::Base
76
+ in_time_scope :published # → Article.in_time_published
77
+ in_time_scope :featured # → Article.in_time_featured
78
+ end
79
+ ```
80
+
81
+ ### カスタムカラム
82
+
83
+ ```ruby
84
+ class Campaign < ActiveRecord::Base
85
+ in_time_scope start_at: { column: :available_at },
86
+ end_at: { column: :expired_at }
87
+ end
88
+ ```
89
+
90
+ ### 開始のみパターン(バージョン履歴)
91
+
92
+ 各行が次の行まで有効なレコード用:
93
+
94
+ ```ruby
95
+ class Price < ActiveRecord::Base
96
+ in_time_scope start_at: { null: false }, end_at: { column: nil }
97
+ end
98
+
99
+ # ボーナス:NOT EXISTSを使った効率的なhas_one
100
+ class User < ActiveRecord::Base
101
+ has_one :current_price, -> { latest_in_time(:user_id) }, class_name: "Price"
102
+ end
103
+
104
+ User.includes(:current_price) # N+1なし、ユーザーごとに最新のみ取得
105
+ ```
106
+
107
+ ### 終了のみパターン(有効期限)
108
+
109
+ 有効期限が切れるまで有効なレコード用:
110
+
111
+ ```ruby
112
+ class Coupon < ActiveRecord::Base
113
+ in_time_scope start_at: { column: nil }, end_at: { null: false }
114
+ end
115
+ ```
116
+
117
+ ### 逆スコープ
118
+
119
+ 時間範囲外のレコードをクエリ:
120
+
121
+ ```ruby
122
+ # まだ開始していないレコード (start_at > time)
123
+ Event.before_in_time
124
+ event.before_in_time?
125
+
126
+ # すでに終了したレコード (end_at <= time)
127
+ Event.after_in_time
128
+ event.after_in_time?
129
+
130
+ # 時間範囲外のレコード(開始前または終了後)
131
+ Event.out_of_time
132
+ event.out_of_time? # in_time?の論理的な逆
133
+ ```
134
+
135
+ 名前付きスコープでも動作:
136
+
137
+ ```ruby
138
+ Article.before_in_time_published # まだ公開されていない
139
+ Article.after_in_time_published # 公開終了
140
+ Article.out_of_time_published # 現在公開されていない
141
+ ```
142
+
143
+ ## オプションリファレンス
144
+
145
+ | オプション | デフォルト | 説明 | 例 |
146
+ | --- | --- | --- | --- |
147
+ | `scope_name`(第1引数) | `:in_time` | `in_time_published`のような名前付きスコープ | `in_time_scope :published` |
148
+ | `start_at: { column: }` | `:start_at` | カスタムカラム名、`nil`で無効化 | `start_at: { column: :available_at }` |
149
+ | `end_at: { column: }` | `:end_at` | カスタムカラム名、`nil`で無効化 | `end_at: { column: nil }` |
150
+ | `start_at: { null: }` | 自動検出 | NULL処理を強制 | `start_at: { null: false }` |
151
+ | `end_at: { null: }` | 自動検出 | NULL処理を強制 | `end_at: { null: true }` |
152
+
153
+ ## 使用例
154
+
155
+ - [有効期限付きポイントシステム](./point-system.md) - フルタイムウィンドウパターン
156
+ - [ユーザー名履歴](./user-name-history.md) - 開始のみパターン
157
+
158
+ ## 謝辞
159
+
160
+ [onk/shibaraku](https://github.com/onk/shibaraku)にインスパイアされました。このGemは以下の機能で概念を拡張しています:
161
+
162
+ - 最適化されたクエリのためのスキーマ対応NULL処理
163
+ - モデルごとの複数の名前付きスコープ
164
+ - 開始のみ / 終了のみパターン
165
+ - 効率的な`has_one`アソシエーション用の`latest_in_time` / `earliest_in_time`
166
+ - 逆スコープ: `before_in_time`, `after_in_time`, `out_of_time`
167
+
168
+ ## 開発
169
+
170
+ ```bash
171
+ # 依存関係のインストール
172
+ bin/setup
173
+
174
+ # テスト実行
175
+ bundle exec rspec
176
+
177
+ # Lint実行
178
+ bundle exec rubocop
179
+
180
+ # CLAUDE.mdの生成(AIコーディングアシスタント用)
181
+ npx rulesync generate
182
+ ```
183
+
184
+ このプロジェクトは[rulesync](https://github.com/dyoshikawa/rulesync)を使用してAIアシスタントルールを管理しています。`.rulesync/rules/*.md`を編集し、`npx rulesync generate`を実行して`CLAUDE.md`を更新してください。
185
+
186
+ ## 貢献
187
+
188
+ バグレポートとプルリクエストは[GitHub](https://github.com/kyohah/in_time_scope)で受け付けています。
189
+
190
+ ## ライセンス
191
+
192
+ MITライセンス
@@ -0,0 +1,295 @@
1
+ # 有効期限付きポイントシステムの例
2
+
3
+ この例では、`in_time_scope`を使用して有効期限付きポイントシステムを実装する方法を示します。ポイントは将来有効になるように事前付与でき、cronジョブが不要になります。
4
+
5
+ 参照: [spec/point_system_spec.rb](https://github.com/kyohah/in_time_scope/blob/main/spec/point_system_spec.rb)
6
+
7
+ ## ユースケース
8
+
9
+ - ユーザーが有効期限付きのポイントを獲得(開始日と終了日)
10
+ - ポイントを将来有効になるように事前付与可能(例:月額メンバーシップボーナス)
11
+ - cronジョブなしで任意の時点の有効ポイントを計算
12
+ - 今後のポイント、期限切れポイントなどを検索
13
+
14
+ ## Cronジョブ不要
15
+
16
+ **これが最大の特徴です。** 従来のポイントシステムはスケジュールジョブの悪夢です:
17
+
18
+ ### よくあるCron地獄
19
+
20
+ ```ruby
21
+ # activate_points_job.rb - 毎分実行
22
+ class ActivatePointsJob < ApplicationJob
23
+ def perform
24
+ Point.where(status: "pending")
25
+ .where("start_at <= ?", Time.current)
26
+ .update_all(status: "active")
27
+ end
28
+ end
29
+
30
+ # expire_points_job.rb - 毎分実行
31
+ class ExpirePointsJob < ApplicationJob
32
+ def perform
33
+ Point.where(status: "active")
34
+ .where("end_at <= ?", Time.current)
35
+ .update_all(status: "expired")
36
+ end
37
+ end
38
+
39
+ # さらに必要なもの:
40
+ # - Sidekiq / Delayed Job / Good Job
41
+ # - Redis(Sidekiq用)
42
+ # - Cronまたはwhenever gem
43
+ # - ジョブ失敗の監視
44
+ # - 失敗時のリトライロジック
45
+ # - 重複実行防止のロック機構
46
+ ```
47
+
48
+ ### InTimeScopeを使う方法
49
+
50
+ ```ruby
51
+ # これだけ。ジョブなし。ステータスカラムなし。インフラ不要。
52
+ user.points.in_time.sum(:amount)
53
+ ```
54
+
55
+ **1行。インフラゼロ。常に正確。**
56
+
57
+ ### なぜこれが機能するのか
58
+
59
+ `start_at`と`end_at`カラムがそのまま状態です。`status`カラムは不要で、時間比較はクエリ時に行われます:
60
+
61
+ ```ruby
62
+ # これらすべてがバックグラウンド処理なしで動作:
63
+ user.points.in_time # 現在有効
64
+ user.points.in_time(1.month.from_now) # 来月有効
65
+ user.points.in_time(1.year.ago) # 昨年有効だった(監査!)
66
+ user.points.before_in_time # 保留中(まだ有効でない)
67
+ user.points.after_in_time # 期限切れ
68
+ ```
69
+
70
+ ### 削減できるもの
71
+
72
+ | コンポーネント | Cronベースシステム | InTimeScope |
73
+ |-----------|------------------|-------------|
74
+ | バックグラウンドジョブライブラリ | 必要 | **不要** |
75
+ | ジョブ用Redis/データベース | 必要 | **不要** |
76
+ | ジョブスケジューラ(cron) | 必要 | **不要** |
77
+ | ステータスカラム | 必要 | **不要** |
78
+ | ステータス更新のマイグレーション | 必要 | **不要** |
79
+ | ジョブ失敗の監視 | 必要 | **不要** |
80
+ | リトライロジック | 必要 | **不要** |
81
+ | 競合状態の処理 | 必要 | **不要** |
82
+
83
+ ### ボーナス:タイムトラベルが無料
84
+
85
+ Cronベースのシステムでは、「ユーザーXの1月15日のポイント残高は?」という質問に答えるには複雑な監査ログやイベントソーシングが必要です。
86
+
87
+ InTimeScopeなら:
88
+
89
+ ```ruby
90
+ user.points.in_time(Date.parse("2024-01-15").middle_of_day).sum(:amount)
91
+ ```
92
+
93
+ **履歴クエリがそのまま動きます。** 追加テーブル不要。イベントソーシング不要。複雑さゼロ。
94
+
95
+ ## スキーマ
96
+
97
+ ```ruby
98
+ # Migration
99
+ class CreatePoints < ActiveRecord::Migration[7.0]
100
+ def change
101
+ create_table :points do |t|
102
+ t.references :user, null: false, foreign_key: true
103
+ t.integer :amount, null: false
104
+ t.string :reason, null: false
105
+ t.datetime :start_at, null: false # ポイントが使用可能になる日時
106
+ t.datetime :end_at, null: false # ポイントが期限切れになる日時
107
+ t.timestamps
108
+ end
109
+
110
+ add_index :points, [:user_id, :start_at, :end_at]
111
+ end
112
+ end
113
+ ```
114
+
115
+ ## モデル
116
+
117
+ ```ruby
118
+ class Point < ApplicationRecord
119
+ belongs_to :user
120
+
121
+ # start_atとend_atの両方が必須(完全な時間範囲)
122
+ in_time_scope start_at: { null: false }, end_at: { null: false }
123
+ end
124
+
125
+ class User < ApplicationRecord
126
+ has_many :points
127
+ has_many :in_time_points, -> { in_time }, class_name: "Point"
128
+
129
+ # 月次ボーナスポイントを付与(事前スケジュール)
130
+ def grant_monthly_bonus(amount:, months_valid: 6)
131
+ points.create!(
132
+ amount: amount,
133
+ reason: "Monthly membership bonus",
134
+ start_at: 1.month.from_now, # 来月有効化
135
+ end_at: (1 + months_valid).months.from_now
136
+ )
137
+ end
138
+ end
139
+ ```
140
+
141
+ ### `has_many :in_time_points`の威力
142
+
143
+ このシンプルな1行で、有効ポイントの**N+1問題のないEager Loading**が可能になります:
144
+
145
+ ```ruby
146
+ # 100人のユーザーと有効ポイントをたった2クエリで取得
147
+ users = User.includes(:in_time_points).limit(100)
148
+
149
+ users.each do |user|
150
+ # 追加クエリなし!すでにロード済み。
151
+ total = user.in_time_points.sum(&:amount)
152
+ puts "#{user.name}: #{total} points"
153
+ end
154
+ ```
155
+
156
+ このアソシエーションがないと:
157
+
158
+ ```ruby
159
+ # N+1問題:ユーザー1クエリ + ポイント100クエリ
160
+ users = User.limit(100)
161
+ users.each do |user|
162
+ total = user.points.in_time.sum(:amount) # ユーザーごとにクエリ!
163
+ end
164
+ ```
165
+
166
+ ## 使い方
167
+
168
+ ### 異なる有効期限でポイントを付与
169
+
170
+ ```ruby
171
+ user = User.find(1)
172
+
173
+ # 即時ポイント(1年間有効)
174
+ user.points.create!(
175
+ amount: 100,
176
+ reason: "Welcome bonus",
177
+ start_at: Time.current,
178
+ end_at: 1.year.from_now
179
+ )
180
+
181
+ # 6ヶ月会員向けの事前スケジュールポイント
182
+ # ポイントは来月有効化、有効化後6ヶ月間有効
183
+ user.grant_monthly_bonus(amount: 500, months_valid: 6)
184
+
185
+ # キャンペーンポイント(期間限定)
186
+ user.points.create!(
187
+ amount: 200,
188
+ reason: "Summer campaign",
189
+ start_at: Date.parse("2024-07-01").beginning_of_day,
190
+ end_at: Date.parse("2024-08-31").end_of_day
191
+ )
192
+ ```
193
+
194
+ ### ポイントの検索
195
+
196
+ ```ruby
197
+ # 現在の有効ポイント
198
+ user.in_time_member_points.sum(:amount)
199
+ # => 100(ウェルカムボーナスのみ現在有効)
200
+
201
+ # 来月利用可能になるポイント数を確認
202
+ user.in_time_member_points(1.month.from_now).sum(:amount)
203
+ # => 600(ウェルカムボーナス + 月次ボーナス)
204
+
205
+ # 保留中のポイント(スケジュール済みだがまだ有効でない)
206
+ user.points.before_in_time.sum(:amount)
207
+ # => 500(有効化待ちの月次ボーナス)
208
+
209
+ # 期限切れポイント
210
+ user.points.after_in_time.sum(:amount)
211
+
212
+ # すべての無効ポイント(保留中 + 期限切れ)
213
+ user.points.out_of_time.sum(:amount)
214
+ ```
215
+
216
+ ### 管理ダッシュボードクエリ
217
+
218
+ ```ruby
219
+ # 履歴監査:特定日に有効だったポイント
220
+ Point.in_time(Date.parse("2024-01-15").middle_of_day)
221
+ .group(:user_id)
222
+ .sum(:amount)
223
+ ```
224
+
225
+ ## 自動メンバーシップボーナスフロー
226
+
227
+ 6ヶ月プレミアムメンバー向けに、**cronなし、Sidekiqなし、Redisなし、監視なし**で定期ボーナスを設定できます:
228
+
229
+ ```ruby
230
+ # ユーザーがプレミアムに登録したとき、メンバーシップと全ボーナスをアトミックに作成
231
+ ActiveRecord::Base.transaction do
232
+ membership = Membership.create!(user: user, plan: "premium_6_months")
233
+
234
+ # 登録時に6ヶ月分の月次ボーナスを事前作成
235
+ 6.times do |month|
236
+ user.points.create!(
237
+ amount: 500,
238
+ reason: "Premium member bonus - Month #{month + 1}",
239
+ start_at: (month + 1).months.from_now,
240
+ end_at: (month + 7).months.from_now # 各ボーナスは6ヶ月間有効
241
+ )
242
+ end
243
+ end
244
+ # => メンバーシップ + 毎月有効化される6つのポイントレコードを作成
245
+ ```
246
+
247
+ ## この設計が優れている理由
248
+
249
+ ### 正確性
250
+
251
+ - **競合状態なし**: Cronジョブは2回実行されたり、スキップしたり、重複したりする可能性があります。InTimeScopeのクエリは常に決定論的です。
252
+ - **タイミングのずれなし**: Cronは間隔で実行(毎分?5分ごと?)。InTimeScopeはミリ秒単位で正確です。
253
+ - **更新漏れなし**: ジョブの失敗でポイントが不正な状態になる可能性があります。InTimeScopeには破損する状態がありません。
254
+
255
+ ### シンプルさ
256
+
257
+ - **インフラ不要**: Sidekiqを削除。Redisを削除。ジョブ監視を削除。
258
+ - **ステータス変更のマイグレーション不要**: 時間がそのまま状態です。`UPDATE`文は不要。
259
+ - **ジョブログのデバッグ不要**: データベースをクエリするだけで何が起きているかわかります。
260
+
261
+ ### テスト容易性
262
+
263
+ ```ruby
264
+ # Cronベースのテストは面倒:
265
+ travel_to 1.month.from_now do
266
+ ActivatePointsJob.perform_now
267
+ ExpirePointsJob.perform_now
268
+ expect(user.points.active.sum(:amount)).to eq(500)
269
+ end
270
+
271
+ # InTimeScopeのテストは簡単:
272
+ expect(user.points.in_time(1.month.from_now).sum(:amount)).to eq(500)
273
+ ```
274
+
275
+ ### まとめ
276
+
277
+ | 観点 | Cronベース | InTimeScope |
278
+ |--------|-----------|-------------|
279
+ | インフラ | Sidekiq + Redis + Cron | **なし** |
280
+ | ポイント有効化 | バッチジョブ(遅延) | **即時** |
281
+ | 履歴クエリ | 監査ログなしでは不可能 | **組み込み** |
282
+ | タイミング精度 | 分(cron間隔) | **ミリ秒** |
283
+ | デバッグ | ジョブログ + データベース | **データベースのみ** |
284
+ | テスト | タイムトラベル + ジョブ実行 | **クエリのみ** |
285
+ | 障害モード | 多数(ジョブ失敗、競合状態) | **なし** |
286
+
287
+ ## Tips
288
+
289
+ 1. **データベースインデックスを使用** - `[user_id, start_at, end_at]`にインデックスを追加してパフォーマンスを最適化。
290
+
291
+ 2. **登録時にポイントを事前付与** - cronジョブをスケジュールする代わりに。
292
+
293
+ 3. **監査には`in_time(time)`を使用** - 任意の過去時点のポイント残高を確認。
294
+
295
+ 4. **逆スコープと組み合わせ** - 保留中/期限切れポイントを表示する管理ダッシュボードを構築。
@@ -0,0 +1,164 @@
1
+ # ユーザー名履歴の例
2
+
3
+ この例では、`in_time_scope`を使用してユーザー名の履歴を管理し、任意の時点でのユーザー名を取得する方法を示します。
4
+
5
+ 参照: [spec/user_name_history_spec.rb](https://github.com/kyohah/in_time_scope/blob/main/spec/user_name_history_spec.rb)
6
+
7
+ ## ユースケース
8
+
9
+ - ユーザーが表示名を変更できる
10
+ - すべての名前変更の履歴を保持する必要がある
11
+ - 特定の時点で有効だった名前を取得したい(監査ログ、履歴レポートなど)
12
+
13
+ ## スキーマ
14
+
15
+ ```ruby
16
+ # Migration
17
+ class CreateUserNameHistories < ActiveRecord::Migration[7.0]
18
+ def change
19
+ create_table :users do |t|
20
+ t.string :email, null: false
21
+ t.timestamps
22
+ end
23
+
24
+ create_table :user_name_histories do |t|
25
+ t.references :user, null: false, foreign_key: true
26
+ t.string :name, null: false
27
+ t.datetime :start_at, null: false # この名前が有効になった日時
28
+ t.timestamps
29
+ end
30
+
31
+ add_index :user_name_histories, [:user_id, :start_at]
32
+ end
33
+ end
34
+ ```
35
+
36
+ ## モデル
37
+
38
+ ```ruby
39
+ class UserNameHistory < ApplicationRecord
40
+ belongs_to :user
41
+ include InTimeScope
42
+
43
+ # 開始日のみパターン: 各レコードはstart_atから次のレコードまで有効
44
+ in_time_scope start_at: { null: false }, end_at: { column: nil }
45
+ end
46
+
47
+ class User < ApplicationRecord
48
+ has_many :user_name_histories
49
+
50
+ # 現在の名前を取得(開始済みの最新レコード)
51
+ has_one :current_name_history,
52
+ -> { latest_in_time(:user_id) },
53
+ class_name: "UserNameHistory"
54
+
55
+ # 現在の名前を取得する便利メソッド
56
+ def current_name
57
+ current_name_history&.name
58
+ end
59
+
60
+ # 特定時点の名前を取得
61
+ def name_at(time)
62
+ user_name_histories.in_time(time).order(start_at: :desc).first&.name
63
+ end
64
+ end
65
+ ```
66
+
67
+ ## 使い方
68
+
69
+ ### 名前履歴の作成
70
+
71
+ ```ruby
72
+ user = User.create!(email: "alice@example.com")
73
+
74
+ # 初期の名前
75
+ UserNameHistory.create!(
76
+ user: user,
77
+ name: "Alice",
78
+ start_at: Time.parse("2024-01-01")
79
+ )
80
+
81
+ # 名前の変更
82
+ UserNameHistory.create!(
83
+ user: user,
84
+ name: "Alice Smith",
85
+ start_at: Time.parse("2024-06-01")
86
+ )
87
+
88
+ # さらに名前を変更
89
+ UserNameHistory.create!(
90
+ user: user,
91
+ name: "Alice Johnson",
92
+ start_at: Time.parse("2024-09-01")
93
+ )
94
+ ```
95
+
96
+ ### 名前の取得
97
+
98
+ ```ruby
99
+ # 現在の名前(has_oneとlatest_in_timeを使用)
100
+ user.current_name
101
+ # => "Alice Johnson"
102
+
103
+ # 特定時点の名前
104
+ user.name_at(Time.parse("2024-03-15"))
105
+ # => "Alice"
106
+
107
+ user.name_at(Time.parse("2024-07-15"))
108
+ # => "Alice Smith"
109
+
110
+ user.name_at(Time.parse("2024-10-15"))
111
+ # => "Alice Johnson"
112
+ ```
113
+
114
+ ### 効率的なEager Loading
115
+
116
+ ```ruby
117
+ # ユーザーと現在の名前を一括取得(N+1なし)
118
+ users = User.includes(:current_name_history).limit(100)
119
+
120
+ users.each do |user|
121
+ puts "#{user.email}: #{user.current_name_history&.name}"
122
+ end
123
+ ```
124
+
125
+ ### 有効なレコードの検索
126
+
127
+ ```ruby
128
+ # 現在有効なすべての名前レコード
129
+ UserNameHistory.in_time
130
+ # => 各ユーザーの最新の名前レコードを返す
131
+
132
+ # 特定時点で有効だった名前レコード
133
+ UserNameHistory.in_time(Time.parse("2024-05-01"))
134
+
135
+ # まだ開始していない名前レコード(将来予定)
136
+ UserNameHistory.before_in_time
137
+ ```
138
+
139
+ ## `latest_in_time`の仕組み
140
+
141
+ `latest_in_time(:user_id)`スコープは効率的な`NOT EXISTS`サブクエリを生成します:
142
+
143
+ ```sql
144
+ SELECT * FROM user_name_histories AS h
145
+ WHERE h.start_at <= '2024-10-01'
146
+ AND NOT EXISTS (
147
+ SELECT 1 FROM user_name_histories AS newer
148
+ WHERE newer.user_id = h.user_id
149
+ AND newer.start_at <= '2024-10-01'
150
+ AND newer.start_at > h.start_at
151
+ )
152
+ ```
153
+
154
+ これにより、指定時点で有効だったユーザーごとの最新レコードのみが返され、`has_one`アソシエーションに最適です。
155
+
156
+ ## Tips
157
+
158
+ 1. **`has_one`では必ず`latest_in_time`を使用** - 外部キーごとに1レコードのみが取得されることを保証します。
159
+
160
+ 2. **`[user_id, start_at]`の複合インデックスを追加** - クエリパフォーマンスを最適化します。
161
+
162
+ 3. **Eager Loadingには`includes`を使用** - `NOT EXISTS`パターンはRailsのEager Loadingと効率的に連携します。
163
+
164
+ 4. **`[user_id, start_at]`にユニーク制約を追加することを検討** - 同時刻の重複レコードを防止します。
@@ -0,0 +1,5 @@
1
+ # Summary
2
+
3
+ - [Introduction](./index.md)
4
+ - [Point System with Expiration](./point-system.md)
5
+ - [User Name History](./user-name-history.md)