historiographer 1.3.0 → 1.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +107 -36
- data/VERSION +1 -1
- data/historiographer.gemspec +3 -3
- data/lib/historiographer.rb +1 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f2d85a348f2e7196cccef533c9562b53c9449df6a0dde03509a1d48a5d66c490
|
4
|
+
data.tar.gz: f3b9f822e53fdadc04d20a0f4066b8b47e019c11333d9dccc0140ea1e9f12991
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5011fe73d7a09abd888f362b1e3923436c5f29068f9b46650ac4e39f09ab3226d7f2d66072293d9908dc37693a7afc5b73f73c2db2c1f6f1b0e9119c153c511b
|
7
|
+
data.tar.gz: ccca124547a124739d68183b9374ddd1b8fc452ed29b568da485981abb37cab8f055384065b00016981ea3844177c5e7e050d0fb6e5f8faef7844e5835387591
|
data/README.md
CHANGED
@@ -1,40 +1,99 @@
|
|
1
|
-
#
|
1
|
+
# Historiographer
|
2
2
|
|
3
|
-
Losing data sucks. Every time you update a record in Rails,
|
3
|
+
Losing data sucks. Every time you update or destroy a record in Rails, you lose the old data.
|
4
4
|
|
5
|
-
|
5
|
+
Historiographer fixes this problem in a better way than existing auditing gems.
|
6
6
|
|
7
7
|
## Existing auditing gems for Rails suck
|
8
8
|
|
9
9
|
The Audited gem has some serious flaws.
|
10
10
|
|
11
|
-
|
11
|
+
🤚Hands up if your `versions` table has gotten too big to query 🤚
|
12
|
+
|
13
|
+
🤚Hands up if your `versions` table doesn't have the indexes you need 🤚
|
14
|
+
|
15
|
+
🤚Hands up if you've ever iterated over `versions` records in Ruby to recreate a snapshot of what data looked like at a point in time. 🤚
|
16
|
+
|
17
|
+
Why does this happen?
|
18
|
+
|
19
|
+
First, `audited` only tracks a record of what changed, so there's no way to "go back in time" and see what the data looked like back when a problem occurred without replaying every single audit.
|
12
20
|
|
13
21
|
Second, it tracks changes as JSON. While some data stores have JSON querying semantics, not all do, making it very hard to ask complex questions of your historical data -- that's the whole reason you're keeping it around.
|
14
22
|
|
15
|
-
Third, it doesn't maintain
|
23
|
+
Third, it doesn't maintain indexes on your data. If you maintain an index on the primary table, wouldn't you also want to look up historical records using the same columns? Historical data is MUCH larger than "latest snapshot" data, so, duh, of course you do.
|
24
|
+
|
25
|
+
Finally, Audited creates just one table for all audits. Historical data is big. It's not unusual for an audited gem table to get into the many millions of rows, and need to be constantly partitioned to maintain any kind of efficiency.
|
26
|
+
|
27
|
+
## How does Historiographer solve these problems?
|
28
|
+
|
29
|
+
Historiographer introduces the concept of _history tables:_ append-only tables that have the same structure and indexes as your primary table.
|
30
|
+
|
31
|
+
If you have a `posts` table:
|
32
|
+
|
33
|
+
| id | title |
|
34
|
+
| :----------- | :----------- |
|
35
|
+
| 1 | My Great Post |
|
36
|
+
| 2 | My Second Post |
|
16
37
|
|
17
|
-
|
38
|
+
You'll also have a `post_histories_table`:
|
18
39
|
|
19
|
-
|
40
|
+
| id | post_id | title | history_started_at | history_ended_at | history_user_id |
|
41
|
+
| :----------- | :----------- | :----------- | :----------- | :----------- | :----------- |
|
42
|
+
| 1 | 1 | My Great Post | '2019-11-08' | NULL | 1 |
|
43
|
+
| 2 | 2| My Second Post | '2019-11-08' | NULL | 1 |
|
20
44
|
|
21
|
-
|
45
|
+
If you change the title of the 1st post:
|
22
46
|
|
23
|
-
|
47
|
+
```Post.find(1).update(title: "Title With Better SEO", history_user_id: current_user.id)```
|
24
48
|
|
25
|
-
|
49
|
+
You'll expect your `posts` table to be updated directly:
|
26
50
|
|
27
|
-
|
51
|
+
| id | title |
|
52
|
+
| :----------- | :----------- |
|
53
|
+
| 1 | Title With Better SEO |
|
54
|
+
| 2 | My Second Post |
|
28
55
|
|
29
|
-
|
56
|
+
But also, your `histories` table will be updated:
|
30
57
|
|
31
|
-
|
58
|
+
| id | post_id | title | history_started_at | history_ended_at | history_user_id |
|
59
|
+
| :----------- | :----------- | :----------- | :----------- | :----------- | :----------- |
|
60
|
+
| 1 | 1 | My Great Post | '2019-11-08' | '2019-11-09' | 1 |
|
61
|
+
| 2 | 2| My Second Post | '2019-11-08' | NULL | 1 |
|
62
|
+
| 1 | 1 | Title With Better SEO | '2019-11-09' | NULL | 1 |
|
32
63
|
|
33
|
-
|
64
|
+
A few things have happened here:
|
34
65
|
|
35
|
-
|
66
|
+
1. The primary table (`posts`) is updated directly
|
67
|
+
2. The existing history for `post_id=1` is timestamped when its `history_ended_at`, so that we can see when the post had the title "My Great Post"
|
68
|
+
3. A new history record is appended to the table containing a complete snapshot of the record, and a `NULL` `history_ended_at`. That's because this is the current history.
|
69
|
+
4. A record of _who_ made the change is saved (`history_user_id`). You can join to your users table to see more data.
|
70
|
+
|
71
|
+
# Getting Started
|
72
|
+
|
73
|
+
Whenever you include the `Historiographer` gem in your ActiveRecord model, it allows you to insert, update, or delete data as you normally would.
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
class Post < ActiveRecord::Base
|
77
|
+
include Historiographer
|
78
|
+
end
|
79
|
+
```
|
36
80
|
|
37
|
-
|
81
|
+
By default, Historiographer will require all SQL-backed methods to provide a `history_user_id` to track who made the changes.
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
Post.create(title: "My Post", history_user_id: current_user.id) # => OK
|
85
|
+
Post.create(title: "My Post") # => Error!
|
86
|
+
```
|
87
|
+
|
88
|
+
Many existing models will be better off using `Historiographer::Safe` when getting started, which will not raise an error, but will alert you of locations where your app is missing `history_user_id`.
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
class Post < ActiveRecord::Base
|
92
|
+
include Historiographer::Safe
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
## Create A Migration
|
38
97
|
|
39
98
|
You need a separate table to store histories for each model.
|
40
99
|
|
@@ -80,25 +139,7 @@ Additionally it will add indices on:
|
|
80
139
|
- The same columns that had indices on the original model (e.g. `enabled`)
|
81
140
|
- `history_started_at`, `history_ended_at`, and `history_user_id`
|
82
141
|
|
83
|
-
|
84
|
-
|
85
|
-
Sometimes the generated index names are too long. Just like with standard Rails migrations, you can override the name of the index to fix this problem. To do so, use the `index_names` argument to override individual index names:
|
86
|
-
|
87
|
-
```ruby
|
88
|
-
require "historiographer/postgres_migration"
|
89
|
-
class CreatePostHistories < ActiveRecord::Migration
|
90
|
-
def change
|
91
|
-
create_table :post_histories do |t|
|
92
|
-
t.histories, index_names: {
|
93
|
-
title: "my_index_name",
|
94
|
-
[:compound, :index] => "my_compound_index_name"
|
95
|
-
}
|
96
|
-
end
|
97
|
-
end
|
98
|
-
end
|
99
|
-
```
|
100
|
-
|
101
|
-
## models
|
142
|
+
## Models
|
102
143
|
|
103
144
|
The primary model should include `Historiographer`:
|
104
145
|
|
@@ -127,6 +168,18 @@ p.histories.first.post == p
|
|
127
168
|
# => true
|
128
169
|
```
|
129
170
|
|
171
|
+
## Creating, Updating, and Destroying Data:
|
172
|
+
|
173
|
+
You can just use normal ActiveRecord methods, and all will record histories:
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
Post.create(title: "My Great Title", history_user_id: current_user.id)
|
177
|
+
Post.find_by(title: "My Great Title").update(title: "A New Title", history_user_id: current_user.id)
|
178
|
+
Post.update_all(title: "They're all the same!", history_user_id: current_user.id)
|
179
|
+
Post.last.destroy!(history_user_id: current_user.id)
|
180
|
+
Post.destroy_all(history_user_id: current_user.id)
|
181
|
+
```
|
182
|
+
|
130
183
|
The `histories` classes have a `current` method, which only finds current history records. These records will also be the same as the data in the primary table.
|
131
184
|
|
132
185
|
```ruby
|
@@ -136,7 +189,25 @@ p.current_history
|
|
136
189
|
PostHistory.current
|
137
190
|
```
|
138
191
|
|
192
|
+
### What to do when generated index names are too long
|
193
|
+
|
194
|
+
Sometimes the generated index names are too long. Just like with standard Rails migrations, you can override the name of the index to fix this problem. To do so, use the `index_names` argument to override individual index names:
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
require "historiographer/postgres_migration"
|
198
|
+
class CreatePostHistories < ActiveRecord::Migration
|
199
|
+
def change
|
200
|
+
create_table :post_histories do |t|
|
201
|
+
t.histories, index_names: {
|
202
|
+
title: "my_index_name",
|
203
|
+
[:compound, :index] => "my_compound_index_name"
|
204
|
+
}
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
```
|
209
|
+
|
139
210
|
== Copyright
|
140
211
|
|
141
|
-
Copyright (c) 2016-
|
212
|
+
Copyright (c) 2016-2020 brettshollenberger. See LICENSE.txt for
|
142
213
|
further details.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.3.
|
1
|
+
1.3.1
|
data/historiographer.gemspec
CHANGED
@@ -2,16 +2,16 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: historiographer 1.3.
|
5
|
+
# stub: historiographer 1.3.1 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "historiographer".freeze
|
9
|
-
s.version = "1.3.
|
9
|
+
s.version = "1.3.1"
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib".freeze]
|
13
13
|
s.authors = ["brettshollenberger".freeze]
|
14
|
-
s.date = "2019-11-
|
14
|
+
s.date = "2019-11-11"
|
15
15
|
s.description = "Creates separate tables for each history table".freeze
|
16
16
|
s.email = "brett.shollenberger@gmail.com".freeze
|
17
17
|
s.extra_rdoc_files = [
|
data/lib/historiographer.rb
CHANGED
@@ -2,6 +2,7 @@ require "active_support/all"
|
|
2
2
|
require_relative "./historiographer/history"
|
3
3
|
require_relative "./historiographer/postgres_migration"
|
4
4
|
require_relative "./historiographer/safe"
|
5
|
+
require_relative "./historiographer/relation"
|
5
6
|
|
6
7
|
# Historiographer takes "histories" (think audits or snapshots) of your model whenever you make changes.
|
7
8
|
#
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: historiographer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.3.
|
4
|
+
version: 1.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- brettshollenberger
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-11-
|
11
|
+
date: 2019-11-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|