destroyer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in destroyer.gemspec
4
+ gemspec
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,6 @@
1
+ require "destroyer/version"
2
+ require "destroyer/destroyer"
3
+
4
+ class ActiveRecord::Base
5
+ include Destroyer
6
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module Destroyer
2
+ VERSION = "0.0.1"
3
+ 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
@@ -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
@@ -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