destroyer 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.md +62 -0
- data/Rakefile +1 -0
- data/destroyer.gemspec +25 -0
- data/lib/destroyer.rb +6 -0
- data/lib/destroyer/destroyer.rb +60 -0
- data/lib/destroyer/version.rb +3 -0
- data/spec/models.rb +45 -0
- data/spec/spec_helper.rb +42 -0
- data/spec/tests_spec.rb +148 -0
- metadata +95 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
#Destroyer
|
2
|
+
|
3
|
+
Deletes records(without instantiating the records first) based on a block(which returns an array of ids) given and also recursively deletes all their associated records if they are marked as :dependent => :destroy. It is useful for background processing.
|
4
|
+
|
5
|
+
## Installing
|
6
|
+
|
7
|
+
Add this to your `Gemfile`
|
8
|
+
|
9
|
+
gem 'destroyer'
|
10
|
+
|
11
|
+
##How to use it
|
12
|
+
|
13
|
+
Add `destroyer` with a `lambda` or `Proc` to the model you want to delete records from which returns an array of ids, like this:
|
14
|
+
|
15
|
+
class User < ActiveRecord::Base
|
16
|
+
destroyer lambda { select("id").where(['created_at < ?', Time.now])] }
|
17
|
+
end
|
18
|
+
|
19
|
+
Then, whenever you want to delete the records just call `start_destroyer` on your model, like this:
|
20
|
+
|
21
|
+
User.start_destroyer
|
22
|
+
|
23
|
+
You could also send a new block to `destroyer` method:
|
24
|
+
|
25
|
+
User.destroyer( lambda { User.select('id').where('rol_id = 4') })
|
26
|
+
|
27
|
+
And then, just call `start_destroyer` on the model and it will execute the process with the block that you just passed to `destroyer` method, keep in mind that the original block will not be overwritten, but be sure to execute `start_destroyer` whenever you pass a new block, otherwise this block will be present(because Destroyer uses class instance variables) the next time you call `start_destroyer` and it will try to delete the records with the ids given in the block, or make sure to set `destroyer_block` to `nil` on the model, like this:
|
28
|
+
|
29
|
+
User.destroyer_block = nil
|
30
|
+
|
31
|
+
##Notes
|
32
|
+
|
33
|
+
`destroyer` also accepts a hash of options, the only available option is `batch_size`, it is used to delete all records in batches, by default is 1000, make sure to set it to and empty hash if you modified the value and did not call `start_destroyer`, otherwise it will have the last value the next time you call the `start_destroyer` method.
|
34
|
+
|
35
|
+
If you do not specify a default block, and later in the code you call your Model.destroyer with a block, that block will become the default block.
|
36
|
+
|
37
|
+
|
38
|
+
##Example
|
39
|
+
|
40
|
+
class PurchaseOrder < ActiveRecord::Base
|
41
|
+
has_many :line_items, :dependent => :destroy
|
42
|
+
destroyer lambda { select("id").where(["state = 'deleted' AND created_at < ?", 1.month.ago]) }
|
43
|
+
end
|
44
|
+
|
45
|
+
class LineItem < ActiveRecord::Base
|
46
|
+
has_many :variant_line_items
|
47
|
+
belongs_to :purchase_order
|
48
|
+
end
|
49
|
+
|
50
|
+
class VariantLineItem < ActiveRecord::Base
|
51
|
+
belongs_to :line_item
|
52
|
+
end
|
53
|
+
|
54
|
+
PurchaseOrder.start_destroyer
|
55
|
+
|
56
|
+
And that code is going to delete all purchase orders which 'state' is 'deleted' and are older that one month ago, and it will also delete all its related line items as well as all their variant line items.
|
57
|
+
|
58
|
+
# About the Author
|
59
|
+
|
60
|
+
[Crowd Interactive](http://www.crowdint.com) is a web design and development company that happens to work in Colima, Mexico.
|
61
|
+
We specialize in building and growing online retail stores. We don’t work with everyone – just companies we believe in. Call us today to see if there’s a fit.
|
62
|
+
Find more info [here](http://www.crowdint.com)!
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/destroyer.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "destroyer/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "destroyer"
|
7
|
+
s.version = Destroyer::VERSION
|
8
|
+
s.authors = ["Sergio Figueroa"]
|
9
|
+
s.email = ["sergio.figueroa@crowdint.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Deletes records without instantiating the records first}
|
12
|
+
s.description = %q{Deletes records(without instantiating the records first) based on a block(which returns an array of ids) given and also recursively deletes all their associated records if they are marked as :dependent => :destroy. It is useful for background processing.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "destroyer"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_dependency 'activerecord', '~> 3.0.0'
|
22
|
+
|
23
|
+
s.add_development_dependency 'sqlite3'
|
24
|
+
s.add_development_dependency 'rspec'
|
25
|
+
end
|
data/lib/destroyer.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
module Destroyer
|
2
|
+
def self.included(target)
|
3
|
+
target.extend Destroyer::ClassMethods
|
4
|
+
end
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
attr_accessor :destroyer_block, :destroyer_options
|
8
|
+
|
9
|
+
def destroyer(block, options = {})
|
10
|
+
@destroyer_block = block
|
11
|
+
@destroyer_default_block ||= @destroyer_block
|
12
|
+
@destroyer_options = options
|
13
|
+
@destroyer_default_options ||= @destroyer_options
|
14
|
+
end
|
15
|
+
|
16
|
+
def start_destroyer
|
17
|
+
return unless @destroyer_default_block
|
18
|
+
let_the_destroyer_starts((@destroyer_block || @destroyer_default_block).call)
|
19
|
+
@destroyer_block = nil
|
20
|
+
@destroyer_options = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def has_one_with_destroy
|
24
|
+
@has_one_with_destroy ||= reflect_on_all_associations(:has_one).select {|a| a.options[:dependent] == :destroy && !a.options[:through] }.map(&:klass)
|
25
|
+
end
|
26
|
+
|
27
|
+
def has_many_with_destroy
|
28
|
+
@has_many_with_destroy ||= reflect_on_all_associations(:has_many).select {|a| a.options[:dependent] == :destroy && !a.options[:through] }.map(&:klass)
|
29
|
+
end
|
30
|
+
|
31
|
+
def has_many_through_with_destroy
|
32
|
+
@has_many_through_with_destroy ||= reflect_on_all_associations(:has_many).select {|a| a.options[:dependent] == :destroy && a.options[:through] }.map(&:through_reflection).map(&:klass)
|
33
|
+
end
|
34
|
+
|
35
|
+
def all_destroyables
|
36
|
+
has_one_with_destroy + has_many_with_destroy + has_many_through_with_destroy
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def let_the_destroyer_starts(ids)
|
41
|
+
destroy_associations(self, ids)
|
42
|
+
ids.each_slice(destroyer_batch_size) {|group| delete_all(["#{primary_key} IN (?)", group])}
|
43
|
+
end
|
44
|
+
|
45
|
+
def destroy_associations(_class, ids)
|
46
|
+
_class.all_destroyables.each do |association|
|
47
|
+
_foreign_key = _class.reflect_on_association(association).try(:options).try(:[], :foreign_key) || "#{_class.table_name.singularize}_id"
|
48
|
+
association.select("#{association.primary_key}").where(["#{_foreign_key} IN (?)", ids])
|
49
|
+
.find_in_batches(:batch_size => destroyer_batch_size) do |association_ids|
|
50
|
+
destroy_associations(association, association_ids.map(&:id))
|
51
|
+
association.delete_all(["#{_foreign_key} IN (?)", ids])
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def destroyer_batch_size
|
57
|
+
@destroyer_options[:batch_size] || @destroyer_default_options[:batch_size] || 1000
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/spec/models.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
class User < ActiveRecord::Base
|
2
|
+
has_one :avatar, :dependent => :destroy
|
3
|
+
has_many :posts, :dependent => :destroy
|
4
|
+
has_one :avatar_history, :through => :avatar
|
5
|
+
|
6
|
+
destroyer lambda { [select("id").first] }
|
7
|
+
end
|
8
|
+
|
9
|
+
class Category < ActiveRecord::Base
|
10
|
+
has_many :categories_posts
|
11
|
+
has_many :posts, :through => :categories_posts
|
12
|
+
end
|
13
|
+
|
14
|
+
class CategoriesPost < ActiveRecord::Base
|
15
|
+
belongs_to :category
|
16
|
+
belongs_to :post
|
17
|
+
end
|
18
|
+
|
19
|
+
class Post < ActiveRecord::Base
|
20
|
+
belongs_to :user
|
21
|
+
|
22
|
+
has_many :categories_posts
|
23
|
+
has_many :categories, :through => :categories_posts, :dependent => :destroy, :class_name => "Category"
|
24
|
+
has_many :comments, :dependent => :destroy
|
25
|
+
|
26
|
+
destroyer lambda { [select("id").first] }
|
27
|
+
end
|
28
|
+
|
29
|
+
class Comment < ActiveRecord::Base
|
30
|
+
belongs_to :post
|
31
|
+
has_one :chair, :dependent => :destroy
|
32
|
+
end
|
33
|
+
|
34
|
+
class Chair < ActiveRecord::Base
|
35
|
+
belongs_to :comment
|
36
|
+
end
|
37
|
+
|
38
|
+
class Avatar < ActiveRecord::Base
|
39
|
+
belongs_to :user
|
40
|
+
has_one :avatar_history
|
41
|
+
end
|
42
|
+
|
43
|
+
class AvatarHistory < ActiveRecord::Base
|
44
|
+
belongs_to :avatar
|
45
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
|
3
|
+
require 'active_record'
|
4
|
+
require 'destroyer'
|
5
|
+
|
6
|
+
RSpec.configure do |config|
|
7
|
+
config.before do
|
8
|
+
ActiveRecord::Base.connection.execute("DELETE FROM users")
|
9
|
+
ActiveRecord::Base.connection.execute("DELETE FROM posts")
|
10
|
+
ActiveRecord::Base.connection.execute("DELETE FROM comments")
|
11
|
+
ActiveRecord::Base.connection.execute("DELETE FROM avatars")
|
12
|
+
ActiveRecord::Base.connection.execute("DELETE FROM categories_posts")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
|
17
|
+
|
18
|
+
ActiveRecord::Schema.define(:version => 0) do
|
19
|
+
create_table :users, :force => true do |t|
|
20
|
+
end
|
21
|
+
create_table :categories, :force => true do |t|
|
22
|
+
end
|
23
|
+
create_table :posts, :force => true do |t|
|
24
|
+
t.integer :user_id
|
25
|
+
end
|
26
|
+
create_table :categories_posts, :force => true do |t|
|
27
|
+
t.integer :category_id
|
28
|
+
t.integer :post_id
|
29
|
+
end
|
30
|
+
create_table :comments, :force => true do |t|
|
31
|
+
t.integer :post_id
|
32
|
+
end
|
33
|
+
create_table :avatars, :force => true do |t|
|
34
|
+
t.integer :user_id
|
35
|
+
end
|
36
|
+
create_table :avatar_histories, :force => true do |t|
|
37
|
+
t.integer :avatar_id
|
38
|
+
end
|
39
|
+
create_table :chairs, :force => true do |t|
|
40
|
+
t.integer :comment_id
|
41
|
+
end
|
42
|
+
end
|
data/spec/tests_spec.rb
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'models'
|
3
|
+
require 'active_support'
|
4
|
+
|
5
|
+
ActiveRecord::Base.logger = ActiveSupport::BufferedLogger.new(STDOUT)
|
6
|
+
|
7
|
+
categories = [Category.create, Category.create]
|
8
|
+
|
9
|
+
describe Destroyer do
|
10
|
+
describe "methods" do
|
11
|
+
it "adds destroyer method to ActiveRecord::Base" do
|
12
|
+
ActiveRecord::Base.public_methods.include?(:destroyer).should be_true
|
13
|
+
end
|
14
|
+
it "adds has_one_with_destroy method to ActiveRecord::Base" do
|
15
|
+
ActiveRecord::Base.public_methods.include?(:has_one_with_destroy).should be_true
|
16
|
+
end
|
17
|
+
it "adds has_many_with_destroy method to ActiveRecord::Base" do
|
18
|
+
ActiveRecord::Base.public_methods.include?(:has_many_with_destroy).should be_true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context "has_one_with_destroy" do
|
23
|
+
describe "User" do
|
24
|
+
it "returns [Avatar]" do
|
25
|
+
User.has_one_with_destroy.should == [Avatar]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
describe "Avatar" do
|
29
|
+
it "returns []" do
|
30
|
+
Avatar.has_one_with_destroy.eql?([]).should be_true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
describe "Category" do
|
34
|
+
it "returns []" do
|
35
|
+
Category.has_one_with_destroy.empty?.should be_true
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context "has_many_with_destroy" do
|
41
|
+
describe "User" do
|
42
|
+
it "returns [Post]" do
|
43
|
+
User.has_many_with_destroy.eql?([Post]).should be_true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
describe "Category" do
|
47
|
+
it "returns []" do
|
48
|
+
Category.has_many_with_destroy.empty?.should be_true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
describe "Post" do
|
52
|
+
it "returns [Comment]" do
|
53
|
+
Post.has_many_with_destroy.eql?([Comment]).should be_true
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context "has_many_through_with_destroy" do
|
59
|
+
describe "Post" do
|
60
|
+
it "returns [CategoriesPost]" do
|
61
|
+
Post.has_many_through_with_destroy.eql?([CategoriesPost]).should be_true
|
62
|
+
end
|
63
|
+
end
|
64
|
+
describe "Category" do
|
65
|
+
it "returns []" do
|
66
|
+
Category.has_many_with_destroy.empty?.should be_true
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context "destroyer in action" do
|
72
|
+
before do
|
73
|
+
# five Users and one Avatar per user
|
74
|
+
# two posts per user = ten posts
|
75
|
+
# three comments per post = thirty comments
|
76
|
+
5.times do
|
77
|
+
user = User.create(:avatar => Avatar.create(:avatar_history => AvatarHistory.create))
|
78
|
+
2.times do
|
79
|
+
post = Post.create(:user => user, :categories => [categories[rand(2)]])
|
80
|
+
3.times do
|
81
|
+
Chair.create(:comment => Comment.create(:post => post))
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context "has_hone relationships" do
|
88
|
+
describe "default block" do
|
89
|
+
it "destroys one user and its avatar" do
|
90
|
+
User.start_destroyer
|
91
|
+
|
92
|
+
User.count.should be(4)
|
93
|
+
Avatar.count.should be(4)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe "passing a new block" do
|
98
|
+
it "destroys the users with the ids given in the block, and also destroys their avatar" do
|
99
|
+
users_to_destroy = User.first(2)
|
100
|
+
user_ids = users_to_destroy.map(&:id)
|
101
|
+
avatars_to_destroy = Avatar.where(["user_id IN (?)", user_ids])
|
102
|
+
|
103
|
+
User.destroyer( lambda { user_ids })
|
104
|
+
User.start_destroyer
|
105
|
+
|
106
|
+
User.count.should be(3)
|
107
|
+
Avatar.count.should be(3)
|
108
|
+
|
109
|
+
User.all.map(&:id).any? {|id| user_ids.include?(id)}.should be_false
|
110
|
+
Avatar.all.map(&:id).any? {|id| avatars_to_destroy.map(&:id).include?(id)}.should be_false
|
111
|
+
end
|
112
|
+
|
113
|
+
describe "when the second time a new block is not given" do
|
114
|
+
it "destroys also the first User and its Avatar" do
|
115
|
+
first_user = User.first(:include => :avatar)
|
116
|
+
last_user = User.last(:include => :avatar)
|
117
|
+
|
118
|
+
User.destroyer( lambda { [User.last] } )
|
119
|
+
User.start_destroyer
|
120
|
+
|
121
|
+
User.start_destroyer
|
122
|
+
|
123
|
+
User.all.any? {|user| [first_user.id, last_user.id].include?(user.id) }.should be_false
|
124
|
+
Avatar.all.any? {|avatar| [first_user.avatar.id, last_user.avatar.id].include?(avatar.id) }.should be_false
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
context "has_many through" do
|
131
|
+
describe "default block" do
|
132
|
+
it "destroys one post and its comments" do
|
133
|
+
Post.start_destroyer
|
134
|
+
|
135
|
+
Post.count.should == 9
|
136
|
+
Comment.count.should == 27
|
137
|
+
end
|
138
|
+
it "destroys one record in the categories_posts table, and does not removes any Category" do
|
139
|
+
Post.start_destroyer
|
140
|
+
|
141
|
+
Post.count.should == 9
|
142
|
+
CategoriesPost.count.should == 9
|
143
|
+
Category.count.should == 2
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
metadata
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: destroyer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Sergio Figueroa
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-04-17 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activerecord
|
16
|
+
requirement: &2157609960 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.0.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2157609960
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: sqlite3
|
27
|
+
requirement: &2157609540 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2157609540
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rspec
|
38
|
+
requirement: &2157609080 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *2157609080
|
47
|
+
description: Deletes records(without instantiating the records first) based on a block(which
|
48
|
+
returns an array of ids) given and also recursively deletes all their associated
|
49
|
+
records if they are marked as :dependent => :destroy. It is useful for background
|
50
|
+
processing.
|
51
|
+
email:
|
52
|
+
- sergio.figueroa@crowdint.com
|
53
|
+
executables: []
|
54
|
+
extensions: []
|
55
|
+
extra_rdoc_files: []
|
56
|
+
files:
|
57
|
+
- .gitignore
|
58
|
+
- Gemfile
|
59
|
+
- README.md
|
60
|
+
- Rakefile
|
61
|
+
- destroyer.gemspec
|
62
|
+
- lib/destroyer.rb
|
63
|
+
- lib/destroyer/destroyer.rb
|
64
|
+
- lib/destroyer/version.rb
|
65
|
+
- spec/models.rb
|
66
|
+
- spec/spec_helper.rb
|
67
|
+
- spec/tests_spec.rb
|
68
|
+
homepage: ''
|
69
|
+
licenses: []
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options: []
|
72
|
+
require_paths:
|
73
|
+
- lib
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ! '>='
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
requirements: []
|
87
|
+
rubyforge_project: destroyer
|
88
|
+
rubygems_version: 1.8.15
|
89
|
+
signing_key:
|
90
|
+
specification_version: 3
|
91
|
+
summary: Deletes records without instantiating the records first
|
92
|
+
test_files:
|
93
|
+
- spec/models.rb
|
94
|
+
- spec/spec_helper.rb
|
95
|
+
- spec/tests_spec.rb
|