in_time_scope 0.1.5 → 0.1.6

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/de/index.md ADDED
@@ -0,0 +1,192 @@
1
+ # InTimeScope
2
+
3
+ Schreiben Sie das jedes Mal in 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
+ Das war's. Eine Zeile DSL, kein rohes SQL in Ihren Models.
18
+
19
+ ## Warum dieses Gem?
20
+
21
+ Dieses Gem existiert, um:
22
+
23
+ - **Zeitbereich-Logik konsistent zu halten** in Ihrer gesamten Codebasis
24
+ - **Copy-Paste SQL zu vermeiden**, das leicht falsch geschrieben wird
25
+ - **Zeit zu einem erstklassigen Domain-Konzept zu machen** mit benannten Scopes wie `in_time_published`
26
+ - **Nullfähigkeit automatisch zu erkennen** aus Ihrem Schema für optimierte Abfragen
27
+
28
+ ## Empfohlen für
29
+
30
+ - Neue Rails-Anwendungen mit Gültigkeitszeiträumen
31
+ - Models mit `start_at` / `end_at` Spalten
32
+ - Teams, die konsistente Zeitlogik ohne verstreute `where`-Klauseln wollen
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ bundle add in_time_scope
38
+ ```
39
+
40
+ ## Schnellstart
41
+
42
+ ```ruby
43
+ class Event < ActiveRecord::Base
44
+ in_time_scope
45
+ end
46
+
47
+ # Klassen-Scope
48
+ Event.in_time # Aktuell aktive Datensätze
49
+ Event.in_time(Time.parse("2024-06-01")) # Zu einem bestimmten Zeitpunkt aktive Datensätze
50
+
51
+ # Instanz-Methode
52
+ event.in_time? # Ist dieser Datensatz jetzt aktiv?
53
+ event.in_time?(some_time) # War er zu diesem Zeitpunkt aktiv?
54
+ ```
55
+
56
+ ## Funktionen
57
+
58
+ ### Auto-optimiertes SQL
59
+
60
+ Das Gem liest Ihr Schema und generiert das richtige SQL:
61
+
62
+ ```ruby
63
+ # NULL-erlaubte Spalten → NULL-bewusste Abfrage
64
+ WHERE (start_at IS NULL OR start_at <= ?) AND (end_at IS NULL OR end_at > ?)
65
+
66
+ # NOT NULL Spalten → einfache Abfrage
67
+ WHERE start_at <= ? AND end_at > ?
68
+ ```
69
+
70
+ ### Benannte Scopes
71
+
72
+ Mehrere Zeitfenster pro Model:
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
+ ### Benutzerdefinierte Spalten
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
+ ### Nur-Start-Muster (Versionshistorie)
91
+
92
+ Für Datensätze, bei denen jede Zeile bis zur nächsten gültig ist:
93
+
94
+ ```ruby
95
+ class Price < ActiveRecord::Base
96
+ in_time_scope start_at: { null: false }, end_at: { column: nil }
97
+ end
98
+
99
+ # Bonus: effizientes has_one mit NOT EXISTS
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) # Kein N+1, holt nur den neuesten pro Benutzer
105
+ ```
106
+
107
+ ### Nur-Ende-Muster (Ablauf)
108
+
109
+ Für Datensätze, die aktiv sind, bis sie ablaufen:
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
+ ### Inverse Scopes
118
+
119
+ Datensätze außerhalb des Zeitfensters abfragen:
120
+
121
+ ```ruby
122
+ # Noch nicht gestartete Datensätze (start_at > time)
123
+ Event.before_in_time
124
+ event.before_in_time?
125
+
126
+ # Bereits beendete Datensätze (end_at <= time)
127
+ Event.after_in_time
128
+ event.after_in_time?
129
+
130
+ # Datensätze außerhalb des Zeitfensters (vor ODER nach)
131
+ Event.out_of_time
132
+ event.out_of_time? # Logische Umkehrung von in_time?
133
+ ```
134
+
135
+ Funktioniert auch mit benannten Scopes:
136
+
137
+ ```ruby
138
+ Article.before_in_time_published # Noch nicht veröffentlicht
139
+ Article.after_in_time_published # Veröffentlichung beendet
140
+ Article.out_of_time_published # Derzeit nicht veröffentlicht
141
+ ```
142
+
143
+ ## Optionen-Referenz
144
+
145
+ | Option | Standard | Beschreibung | Beispiel |
146
+ | --- | --- | --- | --- |
147
+ | `scope_name` (1. Arg) | `:in_time` | Benannter Scope wie `in_time_published` | `in_time_scope :published` |
148
+ | `start_at: { column: }` | `:start_at` | Benutzerdefinierter Spaltenname, `nil` zum Deaktivieren | `start_at: { column: :available_at }` |
149
+ | `end_at: { column: }` | `:end_at` | Benutzerdefinierter Spaltenname, `nil` zum Deaktivieren | `end_at: { column: nil }` |
150
+ | `start_at: { null: }` | Auto-Erkennung | NULL-Behandlung erzwingen | `start_at: { null: false }` |
151
+ | `end_at: { null: }` | Auto-Erkennung | NULL-Behandlung erzwingen | `end_at: { null: true }` |
152
+
153
+ ## Beispiele
154
+
155
+ - [Punktesystem mit Ablaufdatum](./point-system.md) - Vollständiges Zeitfenster-Muster
156
+ - [Benutzernamen-Historie](./user-name-history.md) - Nur-Start-Muster
157
+
158
+ ## Danksagungen
159
+
160
+ Inspiriert von [onk/shibaraku](https://github.com/onk/shibaraku). Dieses Gem erweitert das Konzept mit:
161
+
162
+ - Schema-bewusste NULL-Behandlung für optimierte Abfragen
163
+ - Mehrere benannte Scopes pro Model
164
+ - Nur-Start / Nur-Ende Muster
165
+ - `latest_in_time` / `earliest_in_time` für effiziente `has_one` Assoziationen
166
+ - Inverse Scopes: `before_in_time`, `after_in_time`, `out_of_time`
167
+
168
+ ## Entwicklung
169
+
170
+ ```bash
171
+ # Abhängigkeiten installieren
172
+ bin/setup
173
+
174
+ # Tests ausführen
175
+ bundle exec rspec
176
+
177
+ # Linting ausführen
178
+ bundle exec rubocop
179
+
180
+ # CLAUDE.md generieren (für KI-Coding-Assistenten)
181
+ npx rulesync generate
182
+ ```
183
+
184
+ Dieses Projekt verwendet [rulesync](https://github.com/dyoshikawa/rulesync) zur Verwaltung von KI-Assistenten-Regeln. Bearbeiten Sie `.rulesync/rules/*.md` und führen Sie `npx rulesync generate` aus, um `CLAUDE.md` zu aktualisieren.
185
+
186
+ ## Beitragen
187
+
188
+ Bug-Reports und Pull Requests sind willkommen auf [GitHub](https://github.com/kyohah/in_time_scope).
189
+
190
+ ## Lizenz
191
+
192
+ MIT-Lizenz
@@ -0,0 +1,295 @@
1
+ # Beispiel: Punktesystem mit Ablaufdatum
2
+
3
+ Dieses Beispiel zeigt, wie man ein Punktesystem mit Ablaufdaten unter Verwendung von `in_time_scope` implementiert. Punkte können vorab gewährt werden, um in der Zukunft aktiv zu werden, wodurch Cron-Jobs überflüssig werden.
4
+
5
+ Siehe auch: [spec/point_system_spec.rb](https://github.com/kyohah/in_time_scope/blob/main/spec/point_system_spec.rb)
6
+
7
+ ## Anwendungsfall
8
+
9
+ - Benutzer sammeln Punkte mit Gültigkeitszeiträumen (Startdatum und Ablaufdatum)
10
+ - Punkte können vorab gewährt werden, um in der Zukunft aktiviert zu werden (z.B. monatliche Mitgliedschaftsboni)
11
+ - Berechnung gültiger Punkte zu jedem Zeitpunkt ohne Cron-Jobs
12
+ - Abfrage bevorstehender Punkte, abgelaufener Punkte usw.
13
+
14
+ ## Keine Cron-Jobs erforderlich
15
+
16
+ **Das ist DIE Killer-Funktion.** Traditionelle Punktesysteme sind ein Albtraum aus geplanten Jobs:
17
+
18
+ ### Die Cron-Hölle, die Sie kennen
19
+
20
+ ```ruby
21
+ # activate_points_job.rb - läuft jede Minute
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 - läuft jede Minute
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
+ # Und dann brauchen Sie:
40
+ # - Sidekiq / Delayed Job / Good Job
41
+ # - Redis (für Sidekiq)
42
+ # - Cron oder whenever gem
43
+ # - Monitoring für Job-Fehler
44
+ # - Retry-Logik für fehlgeschlagene Jobs
45
+ # - Sperrmechanismen zur Vermeidung von Doppelausführungen
46
+ ```
47
+
48
+ ### Der InTimeScope-Weg
49
+
50
+ ```ruby
51
+ # Das war's. Keine Jobs. Keine Status-Spalte. Keine Infrastruktur.
52
+ user.points.in_time.sum(:amount)
53
+ ```
54
+
55
+ **Eine Zeile. Null Infrastruktur. Immer genau.**
56
+
57
+ ### Warum das funktioniert
58
+
59
+ Die Spalten `start_at` und `end_at` SIND der Status. Es gibt keine Notwendigkeit für eine `status`-Spalte, da der Zeitvergleich zur Abfragezeit erfolgt:
60
+
61
+ ```ruby
62
+ # All das funktioniert ohne Hintergrundverarbeitung:
63
+ user.points.in_time # Aktuell gültig
64
+ user.points.in_time(1.month.from_now) # Nächsten Monat gültig
65
+ user.points.in_time(1.year.ago) # Waren letztes Jahr gültig (Audit!)
66
+ user.points.before_in_time # Ausstehend (noch nicht aktiv)
67
+ user.points.after_in_time # Abgelaufen
68
+ ```
69
+
70
+ ### Was Sie eliminieren
71
+
72
+ | Komponente | Cron-basiertes System | InTimeScope |
73
+ |-----------|------------------|-------------|
74
+ | Hintergrund-Job-Bibliothek | Erforderlich | **Nicht benötigt** |
75
+ | Redis/Datenbank für Jobs | Erforderlich | **Nicht benötigt** |
76
+ | Job-Scheduler (cron) | Erforderlich | **Nicht benötigt** |
77
+ | Status-Spalte | Erforderlich | **Nicht benötigt** |
78
+ | Migration zur Status-Aktualisierung | Erforderlich | **Nicht benötigt** |
79
+ | Monitoring für Job-Fehler | Erforderlich | **Nicht benötigt** |
80
+ | Retry-Logik | Erforderlich | **Nicht benötigt** |
81
+ | Race-Condition-Handling | Erforderlich | **Nicht benötigt** |
82
+
83
+ ### Bonus: Zeitreisen kostenlos
84
+
85
+ Bei Cron-basierten Systemen erfordert die Beantwortung von "Wie viele Punkte hatte Benutzer X am 15. Januar?" komplexes Audit-Logging oder Event Sourcing.
86
+
87
+ Mit InTimeScope:
88
+
89
+ ```ruby
90
+ user.points.in_time(Date.parse("2024-01-15").middle_of_day).sum(:amount)
91
+ ```
92
+
93
+ **Historische Abfragen funktionieren einfach.** Keine zusätzlichen Tabellen. Kein Event Sourcing. Keine Komplexität.
94
+
95
+ ## Schema
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 # Wann Punkte nutzbar werden
106
+ t.datetime :end_at, null: false # Wann Punkte ablaufen
107
+ t.timestamps
108
+ end
109
+
110
+ add_index :points, [:user_id, :start_at, :end_at]
111
+ end
112
+ end
113
+ ```
114
+
115
+ ## Modelle
116
+
117
+ ```ruby
118
+ class Point < ApplicationRecord
119
+ belongs_to :user
120
+
121
+ # Sowohl start_at als auch end_at sind erforderlich (vollständiges Zeitfenster)
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
+ # Monatliche Bonuspunkte gewähren (vorausgeplant)
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, # Wird nächsten Monat aktiviert
135
+ end_at: (1 + months_valid).months.from_now
136
+ )
137
+ end
138
+ end
139
+ ```
140
+
141
+ ### Die Kraft von `has_many :in_time_points`
142
+
143
+ Diese einfache Zeile ermöglicht **N+1-freies Eager Loading** für gültige Punkte:
144
+
145
+ ```ruby
146
+ # 100 Benutzer mit ihren gültigen Punkten in nur 2 Abfragen laden
147
+ users = User.includes(:in_time_points).limit(100)
148
+
149
+ users.each do |user|
150
+ # Keine zusätzlichen Abfragen! Bereits geladen.
151
+ total = user.in_time_points.sum(&:amount)
152
+ puts "#{user.name}: #{total} points"
153
+ end
154
+ ```
155
+
156
+ Ohne diese Assoziation bräuchten Sie:
157
+
158
+ ```ruby
159
+ # N+1-Problem: 1 Abfrage für Benutzer + 100 Abfragen für Punkte
160
+ users = User.limit(100)
161
+ users.each do |user|
162
+ total = user.points.in_time.sum(:amount) # Abfrage pro Benutzer!
163
+ end
164
+ ```
165
+
166
+ ## Verwendung
167
+
168
+ ### Punkte mit verschiedenen Gültigkeitszeiträumen gewähren
169
+
170
+ ```ruby
171
+ user = User.find(1)
172
+
173
+ # Sofortige Punkte (1 Jahr gültig)
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
+ # Vorausgeplante Punkte für 6-Monats-Mitglieder
182
+ # Punkte werden nächsten Monat aktiviert, 6 Monate nach Aktivierung gültig
183
+ user.grant_monthly_bonus(amount: 500, months_valid: 6)
184
+
185
+ # Kampagnenpunkte (zeitlich begrenzt)
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
+ ### Punkte abfragen
195
+
196
+ ```ruby
197
+ # Aktuell gültige Punkte
198
+ user.in_time_member_points.sum(:amount)
199
+ # => 100 (nur der Willkommensbonus ist aktuell aktiv)
200
+
201
+ # Prüfen, wie viele Punkte nächsten Monat verfügbar sein werden
202
+ user.in_time_member_points(1.month.from_now).sum(:amount)
203
+ # => 600 (Willkommensbonus + monatlicher Bonus)
204
+
205
+ # Ausstehende Punkte (geplant aber noch nicht aktiv)
206
+ user.points.before_in_time.sum(:amount)
207
+ # => 500 (monatlicher Bonus wartet auf Aktivierung)
208
+
209
+ # Abgelaufene Punkte
210
+ user.points.after_in_time.sum(:amount)
211
+
212
+ # Alle ungültigen Punkte (ausstehend + abgelaufen)
213
+ user.points.out_of_time.sum(:amount)
214
+ ```
215
+
216
+ ### Admin-Dashboard-Abfragen
217
+
218
+ ```ruby
219
+ # Historisches Audit: Punkte gültig an einem bestimmten Datum
220
+ Point.in_time(Date.parse("2024-01-15").middle_of_day)
221
+ .group(:user_id)
222
+ .sum(:amount)
223
+ ```
224
+
225
+ ## Automatischer Mitgliedschaftsbonus-Ablauf
226
+
227
+ Für 6-Monats-Premium-Mitglieder können Sie wiederkehrende Boni einrichten **ohne Cron, ohne Sidekiq, ohne Redis, ohne Monitoring**:
228
+
229
+ ```ruby
230
+ # Wenn sich ein Benutzer für Premium anmeldet, Mitgliedschaft und alle Boni atomar erstellen
231
+ ActiveRecord::Base.transaction do
232
+ membership = Membership.create!(user: user, plan: "premium_6_months")
233
+
234
+ # Alle 6 monatlichen Boni bei der Anmeldung vorab erstellen
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 # Jeder Bonus 6 Monate gültig
241
+ )
242
+ end
243
+ end
244
+ # => Erstellt Mitgliedschaft + 6 Punktedatensätze, die monatlich aktiviert werden
245
+ ```
246
+
247
+ ## Warum dieses Design überlegen ist
248
+
249
+ ### Korrektheit
250
+
251
+ - **Keine Race Conditions**: Cron-Jobs können zweimal laufen, Ausführungen überspringen oder sich überlappen. InTimeScope-Abfragen sind immer deterministisch.
252
+ - **Kein Timing-Drift**: Cron läuft in Intervallen (jede Minute? alle 5 Minuten?). InTimeScope ist millisekundengenau.
253
+ - **Keine verlorenen Updates**: Job-Fehler können Punkte in falschen Zuständen hinterlassen. InTimeScope hat keinen Zustand, der beschädigt werden kann.
254
+
255
+ ### Einfachheit
256
+
257
+ - **Keine Infrastruktur**: Löschen Sie Sidekiq. Löschen Sie Redis. Löschen Sie das Job-Monitoring.
258
+ - **Keine Migrationen für Status-Änderungen**: Die Zeit IST der Status. Keine `UPDATE`-Anweisungen nötig.
259
+ - **Kein Debugging von Job-Logs**: Fragen Sie einfach die Datenbank ab, um genau zu sehen, was passiert.
260
+
261
+ ### Testbarkeit
262
+
263
+ ```ruby
264
+ # Cron-basiertes Testen ist mühsam:
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-Tests sind trivial:
272
+ expect(user.points.in_time(1.month.from_now).sum(:amount)).to eq(500)
273
+ ```
274
+
275
+ ### Zusammenfassung
276
+
277
+ | Aspekt | Cron-basiert | InTimeScope |
278
+ |--------|-----------|-------------|
279
+ | Infrastruktur | Sidekiq + Redis + Cron | **Keine** |
280
+ | Punkteaktivierung | Batch-Job (verzögert) | **Sofort** |
281
+ | Historische Abfragen | Unmöglich ohne Audit-Log | **Eingebaut** |
282
+ | Timing-Genauigkeit | Minuten (Cron-Intervall) | **Millisekunden** |
283
+ | Debugging | Job-Logs + Datenbank | **Nur Datenbank** |
284
+ | Testen | Zeitreisen + Jobs ausführen | **Nur Abfrage** |
285
+ | Fehlermodi | Viele (Job-Fehler, Race Conditions) | **Keine** |
286
+
287
+ ## Tipps
288
+
289
+ 1. **Verwenden Sie Datenbankindizes** auf `[user_id, start_at, end_at]` für optimale Performance.
290
+
291
+ 2. **Gewähren Sie Punkte vorab bei der Anmeldung** anstatt Cron-Jobs zu planen.
292
+
293
+ 3. **Verwenden Sie `in_time(time)` für Audits**, um Punktestände zu jedem historischen Zeitpunkt zu prüfen.
294
+
295
+ 4. **Kombinieren Sie mit inversen Scopes**, um Admin-Dashboards mit ausstehenden/abgelaufenen Punkten zu erstellen.
@@ -0,0 +1,164 @@
1
+ # Beispiel: Benutzernamen-Historie
2
+
3
+ Dieses Beispiel zeigt, wie man die Benutzernamen-Historie mit `in_time_scope` verwaltet, sodass Sie den Namen eines Benutzers zu jedem Zeitpunkt abfragen können.
4
+
5
+ Siehe auch: [spec/user_name_history_spec.rb](https://github.com/kyohah/in_time_scope/blob/main/spec/user_name_history_spec.rb)
6
+
7
+ ## Anwendungsfall
8
+
9
+ - Benutzer können ihren Anzeigenamen ändern
10
+ - Sie müssen eine Historie aller Namensänderungen aufbewahren
11
+ - Sie möchten den Namen abrufen, der zu einem bestimmten Zeitpunkt aktiv war (z.B. für Audit-Logs, historische Berichte)
12
+
13
+ ## Schema
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 # Wann dieser Name aktiv wurde
28
+ t.timestamps
29
+ end
30
+
31
+ add_index :user_name_histories, [:user_id, :start_at]
32
+ end
33
+ end
34
+ ```
35
+
36
+ ## Modelle
37
+
38
+ ```ruby
39
+ class UserNameHistory < ApplicationRecord
40
+ belongs_to :user
41
+ include InTimeScope
42
+
43
+ # Nur-Start-Muster: Jeder Datensatz ist von start_at bis zum nächsten Datensatz gültig
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
+ # Den aktuellen Namen abrufen (neuester Datensatz, der begonnen hat)
51
+ has_one :current_name_history,
52
+ -> { latest_in_time(:user_id) },
53
+ class_name: "UserNameHistory"
54
+
55
+ # Praktische Methode für den aktuellen Namen
56
+ def current_name
57
+ current_name_history&.name
58
+ end
59
+
60
+ # Namen zu einem bestimmten Zeitpunkt abrufen
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
+ ## Verwendung
68
+
69
+ ### Namenshistorie erstellen
70
+
71
+ ```ruby
72
+ user = User.create!(email: "alice@example.com")
73
+
74
+ # Anfänglicher Name
75
+ UserNameHistory.create!(
76
+ user: user,
77
+ name: "Alice",
78
+ start_at: Time.parse("2024-01-01")
79
+ )
80
+
81
+ # Namensänderung
82
+ UserNameHistory.create!(
83
+ user: user,
84
+ name: "Alice Smith",
85
+ start_at: Time.parse("2024-06-01")
86
+ )
87
+
88
+ # Weitere Namensänderung
89
+ UserNameHistory.create!(
90
+ user: user,
91
+ name: "Alice Johnson",
92
+ start_at: Time.parse("2024-09-01")
93
+ )
94
+ ```
95
+
96
+ ### Namen abfragen
97
+
98
+ ```ruby
99
+ # Aktueller Name (verwendet has_one mit latest_in_time)
100
+ user.current_name
101
+ # => "Alice Johnson"
102
+
103
+ # Name zu einem bestimmten Zeitpunkt
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
+ ### Effizientes Eager Loading
115
+
116
+ ```ruby
117
+ # Benutzer mit ihren aktuellen Namen laden (kein 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
+ ### Aktive Datensätze abfragen
126
+
127
+ ```ruby
128
+ # Alle Namensdatensätze, die aktuell aktiv sind
129
+ UserNameHistory.in_time
130
+ # => Gibt den neuesten Namensdatensatz für jeden Benutzer zurück
131
+
132
+ # Namensdatensätze, die zu einem bestimmten Zeitpunkt aktiv waren
133
+ UserNameHistory.in_time(Time.parse("2024-05-01"))
134
+
135
+ # Namensdatensätze, die noch nicht begonnen haben (für die Zukunft geplant)
136
+ UserNameHistory.before_in_time
137
+ ```
138
+
139
+ ## Wie `latest_in_time` funktioniert
140
+
141
+ Der `latest_in_time(:user_id)` Scope generiert eine effiziente `NOT EXISTS` Unterabfrage:
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
+ Dies gibt nur den neuesten Datensatz pro Benutzer zurück, der zum gegebenen Zeitpunkt aktiv war, was es perfekt für `has_one` Assoziationen macht.
155
+
156
+ ## Tipps
157
+
158
+ 1. **Verwenden Sie immer `latest_in_time` mit `has_one`** - Es stellt sicher, dass Sie genau einen Datensatz pro Fremdschlüssel erhalten.
159
+
160
+ 2. **Fügen Sie einen zusammengesetzten Index hinzu** auf `[user_id, start_at]` für optimale Abfrageleistung.
161
+
162
+ 3. **Verwenden Sie `includes` für Eager Loading** - Das `NOT EXISTS`-Muster funktioniert effizient mit Rails Eager Loading.
163
+
164
+ 4. **Erwägen Sie das Hinzufügen einer Unique-Constraint** auf `[user_id, start_at]`, um doppelte Datensätze zum gleichen Zeitpunkt zu verhindern.
@@ -0,0 +1,5 @@
1
+ # Sommaire
2
+
3
+ - [Introduction](./index.md)
4
+ - [Système de points avec expiration](./point-system.md)
5
+ - [Historique des noms d'utilisateur](./user-name-history.md)