miss_hannigan 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 703b7799f10817fa5d27a8b8627f11223f138decff67f46352881e9fd0cd47b4
4
- data.tar.gz: f8d9ca9e92e88e80de30a110cd1d115b5e8d98560735c7256ff1cefb2bb98b02
3
+ metadata.gz: 3978b43b61c215d99d63cda3090f4112ada5899f5c5a1a633f397c12702fce29
4
+ data.tar.gz: bbd50537d9be7187e700cca4b5220f5202d43f08cb558fa02fc5896c3a80109d
5
5
  SHA512:
6
- metadata.gz: a7f287cbe1d13a6c34c81492a995a1556855f34f5bd7fea7de8d0c8d439db7d715a431cf6a0a1298733e0d78a805eab7726ed1b1f30788f3c4a91db4acabe948
7
- data.tar.gz: 7cdac48b599c565a0e79a75b6043f60d3f55e234696f95d08c8073d6d1c82b858019960df6768a7d08b1feb90f3ed3d5014ef403b0fa8353661aeb89facfb8dc
6
+ metadata.gz: 6161f50401dd02989f211355bc30a13b7e6db01a6f981c43fd9889cdb2ea4c3de79059bdd3bdf3ae0671044575ffcb0d504db3449b343400a0326910d9cdf99e
7
+ data.tar.gz: f813c60fbf646f76c20952ce489b0c7da2757c5b7d2198d56742f663d8128c458918279b7e50c03563f8b3d5e0eba90e52e2c5fcef33ce61463780711bee880b
data/README.md CHANGED
@@ -6,11 +6,11 @@
6
6
 
7
7
  ## What?
8
8
 
9
- miss_hannigan provides an alternative (and in some cases, better) way to do cascading deletes/destroys in Rails. With it, you can now define a :dependent has_many behavior of :nullify_then_purge which will quickly and synchronously nullify (orphan) children from their parent, and then asynchronously purge those child records (the orphans) from the database.
9
+ miss_hannigan provides an alternative (and in some cases, better) way to do cascading deletes/destroys in Rails. With it, you can now define a :dependent has_many behavior of :nullify_then_purge which will quickly and synchronously nullify (orphan) children from their parent, and then asynchronously purge those child records (the orphans) from the database.
10
10
 
11
11
  ```
12
12
  class Parent < ApplicationRecord
13
- has_many :children, dependent: :nullify_then_purge
13
+ has_many :children, dependent: :nullify_then_purge
14
14
  end
15
15
  ```
16
16
 
@@ -19,13 +19,13 @@ end
19
19
  1. Add `gem 'miss_hannigan'` to your Gemfile.
20
20
  2. Run `bundle install`.
21
21
  3. Restart your server
22
- 4. Add the new dependent option to your has_many relationships:
22
+ 4. Add the new dependent option to your has_many relationships:
23
23
 
24
24
  ```
25
25
  has_many :children, dependent: :nullify_then_purge
26
26
  ```
27
27
 
28
- Note: If your `child` has a foreign_key relationship with the `parent`, you'll need to make sure the foreign_key in the `child` allows for nulls. For example, you might have to create migrations like this:
28
+ Note: If your `child` has a foreign_key relationship with the `parent`, you'll need to make sure the foreign_key in the `child` allows for nulls. For example, you might have to create migrations like this:
29
29
 
30
30
  ```
31
31
  class RemoveNullKeyConstraint < ActiveRecord::Migration[6.0]
@@ -35,41 +35,41 @@ class RemoveNullKeyConstraint < ActiveRecord::Migration[6.0]
35
35
  end
36
36
  ```
37
37
 
38
- miss_hannigan will raise an error if the foreign_key isn't configured appropriately.
38
+ miss_hannigan will raise an error if the foreign_key isn't configured appropriately.
39
39
 
40
- miss_hannigan also assumes you're using ActiveJob with an active queue system in place - that's how orphans get asynchronously destroyed after all.
40
+ miss_hannigan also assumes you're using ActiveJob with an active queue system in place - that's how orphans get asynchronously destroyed after all.
41
41
 
42
42
  ## Why?
43
43
 
44
44
  Whether you are a Rails expert or just getting started with the framework, you've most likely had to make smart choices on how cascading deletes work in your system. And often in large systems, you're forced with a compromise...
45
45
 
46
- To quickly catch beginners up, Rails has some great tooling to deal with parent-child relationships using has_many:
46
+ To quickly catch beginners up, Rails has some great tooling to deal with parent-child relationships using has_many:
47
47
 
48
48
  ```
49
49
  class Parent < ApplicationRecord
50
- has_many :children
50
+ has_many :children
51
51
  end
52
52
  ```
53
53
 
54
- By default, what happens to `children` when you delete an instance of Parent? Nothing. Children just sit tight or in our more typical vernacular, they're orphaned.
54
+ By default, what happens to `children` when you delete an instance of Parent? Nothing. Children just sit tight or in our more typical vernacular, they're orphaned.
55
55
 
56
- Normally, you consider two options then: destroy the children, or delete the children.
56
+ Normally, you consider two options then: destroy the children, or delete the children.
57
57
 
58
58
  ### dependent: :destroy
59
59
 
60
- Destroying the children is ideal. You do that by setting `dependent: :destroy` on the has_many relationship. Like so:
60
+ Destroying the children is ideal. You do that by setting `dependent: :destroy` on the has_many relationship. Like so:
61
61
 
62
62
  ```
63
63
  class Parent ApplicationRecord
64
- has_many :children, dependent: :destroy
64
+ has_many :children, dependent: :destroy
65
65
  end
66
66
  ```
67
67
 
68
68
  Rails, when attempting to destroy an instance of the Parent, will also iteratively go through each child of the parent calling destroy on the child. The benefit of this is that any callbacks and validation on those children are given their day in the sun. If you're using a foreign_key constraint between Parent -> Child, this path will also keep your DB happy. (The children are deleted first, then the parent, avoiding the DB complaining about a foreign key being invalid.)
69
69
 
70
- But the main drawback is that destroying a ton of children can be time consuming, especially if those children have their own children (and those have more children, etc.). So time consuming that you simply can't have a user wait that long do even do a delete. And with some hosting platforms, the deletes won't even work as you'll face Timeout errors instead.
70
+ But the main drawback is that destroying a ton of children can be time consuming, especially if those children have their own children (and those have more children, etc.). So time consuming that you simply can't have a user wait that long do even do a delete. And with some hosting platforms, the deletes won't even work as you'll face Timeout errors instead.
71
71
 
72
- So, many of us reach for the much faster option of using a `:delete_all ` dependency.
72
+ So, many of us reach for the much faster option of using a `:delete_all ` dependency.
73
73
 
74
74
  ### dependent: :delete_all
75
75
 
@@ -77,65 +77,65 @@ Going this route, Rails will delete all children of a parent in a single SQL cal
77
77
 
78
78
  ```
79
79
  class Parent ApplicationRecord
80
- has_many :children, dependent: :delete_all
80
+ has_many :children, dependent: :delete_all
81
81
  end
82
82
  ```
83
83
 
84
- However, `:delete` has plenty of problems because it doesn't go through the typical Rails destroy.
84
+ However, `:delete` has plenty of problems because it doesn't go through the typical Rails destroy.
85
85
 
86
86
  For example, you can't automatically do any post-destroy cleanup (e.g. 3rd party API calls) when those children are destroyed.
87
87
 
88
- And you can't use this approach if you are using foreign key constraints in your DB:
88
+ And you can't use this approach if you are using foreign key constraints in your DB:
89
89
 
90
90
  ![](https://github.com/sutrolabs/miss_hannigan/blob/master/foreign_key_error_example.png?raw=true)
91
91
 
92
- Another catch is that if you have a Parent -> Child -> Grandchild relationship, and it uses `dependent: :delete_all` down the tree, destroying a Parent, will stop with deleting the Children. Grandchildren won't even get deleted/destroyed.
92
+ Another catch is that if you have a Parent -> Child -> Grandchild relationship, and it uses `dependent: :delete_all` down the tree, destroying a Parent, will stop with deleting the Children. Grandchildren won't even get deleted/destroyed.
93
93
 
94
94
  ------------
95
95
 
96
- Here at Census this became a problem. We have quite a lot of children of parent objects. And children have children have children... We had users experiencing timeouts during deletions.
96
+ Here at Census this became a problem. We have quite a lot of children of parent objects. And children have children have children... We had users experiencing timeouts during deletions.
97
97
 
98
- Well, we can't reach for dependent: :delete_all since we have a multiple layered hierarchy of objects that all need destroying. We also have foreign_key constraints we'd like to keep using.
98
+ Well, we can't reach for dependent: :delete_all since we have a multiple layered hierarchy of objects that all need destroying. We also have foreign_key constraints we'd like to keep using.
99
99
 
100
- So what do we do if neither of these approaches work for us?
100
+ So what do we do if neither of these approaches work for us?
101
101
 
102
- We use an "orphan then later purge" approach. Which has some of the best of both :destroy and :delete_all worlds.
102
+ We use an "orphan then later purge" approach. Which has some of the best of both :destroy and :delete_all worlds.
103
103
 
104
- dependent has a nifty but less often mentioned option of :nullify.
104
+ dependent has a nifty but less often mentioned option of :nullify.
105
105
 
106
106
  ```
107
107
  class Parent < ApplicationRecord
108
- has_many :children, dependent: :nullify
108
+ has_many :children, dependent: :nullify
109
109
  end
110
110
  ```
111
111
 
112
- Using :nullify will simply issue a single UPDATE statement setting children's parent_id to NULL. Which is super fast.
112
+ Using :nullify will simply issue a single UPDATE statement setting children's parent_id to NULL. Which is super fast.
113
113
 
114
- This sets up a bunch of orphaned children now that can easily be cleaned up in an asynchronous purge.
114
+ This sets up a bunch of orphaned children now that can easily be cleaned up in an asynchronous purge.
115
115
 
116
- And now because we're destroying Children here, the normal callbacks are run also allowing Rails to cleanup and destroy GrandChildren.
116
+ And now because we're destroying Children here, the normal callbacks are run also allowing Rails to cleanup and destroy GrandChildren.
117
117
 
118
- Fast AND thorough.
118
+ Fast AND thorough.
119
119
 
120
- So we wrapped that pattern together into miss_hannigan:
120
+ So we wrapped that pattern together into miss_hannigan:
121
121
 
122
122
  ```
123
123
  class Parent < ApplicationRecord
124
- has_many :children, dependent: :nullify_then_purge
124
+ has_many :children, dependent: :nullify_then_purge
125
125
  end
126
126
  ```
127
127
 
128
- ## Alternatives
128
+ ## Alternatives
129
129
 
130
- It's worth noting there are other strategies like allowing your DB handle its own cascading deletes. For example, adding foreign keys on a Postgres DB from a Rails migration like so:
130
+ It's worth noting there are other strategies like allowing your DB handle its own cascading deletes. For example, adding foreign keys on a Postgres DB from a Rails migration like so:
131
131
 
132
132
  ```
133
133
  add_foreign_key "children", "parents", on_delete: :cascade
134
134
  ```
135
135
 
136
- Doing that will have Postgres automatically delete children rows when a parent is deleted. But that removes itself from Rails-land where we have other cleanup hooks and tooling we'd like to keep running.
136
+ Doing that will have Postgres automatically delete children rows when a parent is deleted. But that removes itself from Rails-land where we have other cleanup hooks and tooling we'd like to keep running.
137
137
 
138
- Another alternative would be to use a pattern like acts_as_paranoid to "soft delete" a parent record and later destroy it asynchronously.
138
+ Another alternative would be to use a pattern like acts_as_paranoid to "soft delete" a parent record and later destroy it asynchronously.
139
139
 
140
140
 
141
141
  Feedback
@@ -145,4 +145,4 @@ Feedback
145
145
 
146
146
  From
147
147
  -----------
148
- :wave: The folks at [Census](http://getcensus.com) originally put this together. Have data? We'll sync your data warehouse with your CRM and the customer success apps critical to your team.
148
+ :wave: The folks at [Census](http://getcensus.com) originally put this together. Have data? We'll sync your data warehouse with your CRM and the customer success apps critical to your team.
@@ -2,43 +2,49 @@ module MissHannigan
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  module ClassMethods
5
+ def has_many(name, scope = nil, **options, &extension)
6
+ nullify_then_purge = detect_nullify_then_purge(options)
7
+ super.tap do |reflection|
8
+ connect_nullify_then_purge(reflection, name) if nullify_then_purge
9
+ end
10
+ end
5
11
 
6
- def has_many(name, scope = nil, **options, &extension)
7
- nullify_then_purge = false
12
+ def has_one(name, scope = nil, **options, &extension)
13
+ nullify_then_purge = detect_nullify_then_purge(options)
14
+ super.tap do |reflection|
15
+ connect_nullify_then_purge(reflection, name) if nullify_then_purge
16
+ end
17
+ end
8
18
 
9
- # we're really just relying on :nullify. so just return our dependent option to that
19
+ def detect_nullify_then_purge(options)
10
20
  if options[:dependent] == :nullify_then_purge
11
- nullify_then_purge = true
12
21
  options[:dependent] = :nullify
22
+ true
23
+ else
24
+ false
13
25
  end
26
+ end
14
27
 
15
- # get our normal has_many reflection to get setup
16
- reflection = super
17
-
18
- if nullify_then_purge
19
-
20
- # has the details of the relation to Child
21
- reflection_details = reflection[name.to_s]
28
+ def connect_nullify_then_purge(reflection, name)
29
+ # has the details of the relation to Child
30
+ reflection_details = reflection[name.to_s]
22
31
 
23
- # I bet folks are going to forget to do the migration of foreign_keys to accept null. Rails defaults
24
- # to not allow null.
25
- if !reflection_details.klass.columns.find { |c| c.name == reflection_details.foreign_key }.null
26
- raise "The foreign key must be nullable to support MissHannigan. You should create a migration to:
27
- change_column_null :#{name.to_s}, :#{reflection_details.foreign_key}, true"
28
- end
29
-
30
- after_destroy do |this_object|
31
- CleanupJob.perform_later(reflection_details.klass.to_s, reflection_details.foreign_key)
32
- end
32
+ # I bet folks are going to forget to do the migration of foreign_keys to accept null. Rails defaults
33
+ # to not allow null.
34
+ if !reflection_details.klass.columns.find { |c| c.name == reflection_details.foreign_key }.null
35
+ raise "The foreign key must be nullable to support MissHannigan. You should create a migration to:
36
+ change_column_null :#{name.to_s}, :#{reflection_details.foreign_key}, true"
33
37
  end
34
38
 
35
- return reflection
39
+ after_destroy do |this_object|
40
+ CleanupJob.perform_later(reflection_details.klass.to_s, reflection_details.foreign_key)
41
+ end
36
42
  end
37
43
  end
38
44
 
39
45
  class CleanupJob < ActiveJob::Base
40
46
  queue_as :default
41
-
47
+
42
48
  def perform(klass_string, parent_foreign_key)
43
49
  klass = klass_string.constantize
44
50
 
@@ -1,3 +1,3 @@
1
1
  module MissHannigan
2
- VERSION = '0.1.1'
2
+ VERSION = '0.1.2'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: miss_hannigan
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - n8
@@ -13,7 +13,7 @@ authors:
13
13
  autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
- date: 2020-02-28 00:00:00.000000000 Z
16
+ date: 2020-08-24 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: rails