association_collection_tools 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,126 @@
1
+ =Association Collection Tools
2
+
3
+ == About
4
+
5
+ Any time you use an ORM you need to know that you are often sacrificing
6
+ performance for convenience and developer efficiency. In general, this
7
+ is a good thing. I agree with the theory espoused by DHH that developer
8
+ productivity is *often* more valuable than machine performance. At least,
9
+ I certainly agree with it in the early stages of development. Once you
10
+ get to a certain scale, however, there are cases where you'll need to
11
+ write your own code that bypasses the ORM in the name of performance.
12
+ This plugin provides some association operations that issue direct SQL
13
+ calls to make things go faster.
14
+
15
+ a. fast_copy
16
+ A method called fast_copy is added to has_and_belongs_to_many association
17
+ collections that makes the process of cloning HABTM associations *MUCH*
18
+ more efficient. Simply replace person1.items = person2.items with
19
+ person1.items.fast_copy(person2) and you're database, network and RAM will
20
+ thank you. See below for more details.
21
+
22
+ b. ids
23
+ A method called ids is added to has_many and has_and_belongs_to_many
24
+ association collections. It returns the list of object ids in the association
25
+ collection without unnecessarily instantiating the objects.
26
+
27
+ == Installation
28
+
29
+ 1. This plugin requires that the memcache-client gem is installed.
30
+ # gem install association_collection_tools
31
+
32
+ 2. Install the plugin OR the gem
33
+ $ script/plugin install svn://rubyforge.org/var/svn/zventstools/projects/association_collection_tools
34
+ - OR -
35
+ # gem install association_collection_tools
36
+
37
+ == HABTM Fast Copy
38
+ Copies a HABTM association collection from one object to another
39
+ without instantiating a bunch of ActiveRecord objects. This is
40
+ faster than the standard assignment operation since:
41
+
42
+ 1. Eliminates massive number of SQL calls used in standard HABTM
43
+ copy by changing it from an O(n) operation to O(1) where
44
+ n is the number of objects in the association collection.
45
+ 2. It transfers only object IDs back and forth between the database
46
+ instead of all object attributes. Resulting in less work for
47
+ the database, less data transferred and less memory used in ruby.
48
+ 3. It doesn't instantiate ActiveRecord objects in memory.
49
+
50
+ A normal HABTM copy (e.g., person1.items = person2.items) results
51
+ in the following SQL calls.
52
+
53
+ SELECT * FROM items INNER JOIN items_people ON items.id = items_people.item_id WHERE (items_people.person_id = 1 )
54
+ SELECT * FROM items INNER JOIN items_people ON items.id = items_people.item_id WHERE (items_people.person_id = 2 )
55
+ DELETE FROM items_people WHERE person_id = 2 AND item_id IN (4)
56
+ INSERT INTO items_people (`item_id`, `person_id`) VALUES (1, 2)
57
+ INSERT INTO items_people (`item_id`, `person_id`) VALUES (2, 2)
58
+ INSERT INTO items_people (`item_id`, `person_id`) VALUES (3, 2)
59
+
60
+ Notice that:
61
+ - items AR objects are instantiated unnecessarily (especially since
62
+ person2.items are about to be deleted)
63
+ - 1 SQL call is issued for each object (item) in the association
64
+ collection (items_people)
65
+
66
+ whereas person.items.fast_copy will result in the
67
+ the following SQL calls greatly reducing the impact on the database
68
+ and on ruby memory utilization.
69
+
70
+ DELETE FROM items_people WHERE person_id = 2
71
+ SELECT item_id FROM items_people WHERE person_id = 1
72
+ REPLACE INTO items_people (person_id,item_id) VALUES (2,3),(2,2),(2,1)
73
+
74
+ Here are some benchmarks:
75
+
76
+ when n = 10 and 26 objects in e2.groups:
77
+
78
+ Benchmark.bm do |x|
79
+ x.report { for i in 1..n; e1.groups.clear;e1.groups = e2.groups;end }
80
+ x.report { for i in 1..n; e1.groups.clear;e1.groups.fast_copy(e2);end }
81
+ end
82
+
83
+ user system total real
84
+ 1.140000 0.040000 1.180000 ( 1.832122)
85
+ 0.020000 0.010000 0.030000 ( 0.125368)
86
+
87
+ when n = 100 and 26 objects in e2.groups:
88
+
89
+ user system total real
90
+ 11.140000 0.360000 11.500000 ( 18.171410)
91
+ 0.140000 0.010000 0.150000 ( 2.368200)
92
+
93
+ This method also supports HABTM join tables with additional attributes.
94
+ Simply pass in an attribute hash as the second argument and it will
95
+ add the attributes to the records it creates in the join table.
96
+
97
+ e.g, person1.items.fast_copy(person2, {:created_at => Time.now})
98
+
99
+ REALITY CHECK: The HABTM docs refer to collection_singular_ids=ids
100
+ which implies identical functionality, but I can't find mention of
101
+ this method in anything other than the documentation. Maybe this
102
+ actually already exists and I'm just blind, but from the looks of
103
+ http://dev.rubyonrails.org/ticket/2917, it appears that it is a
104
+ documentation bug.
105
+
106
+ == HABTM and has_many ids
107
+ Return the list of IDs in this association collection without unnecessarily
108
+ instantiating a bunch of Active Record objects. What good is the id of
109
+ an object without the object itself? If you think about it for a while,
110
+ you're bound to come up with many uses, especially if you write a lot of
111
+ SQL by hand. For instance, the fast_copy command documented above uses
112
+ this method to return an id list without instantiating AR objects. The
113
+ potential savings are enormous when you're dealing with hundreds or thousands
114
+ of objects at a time.
115
+
116
+ == Bugs, Code and Contributing
117
+
118
+ There.s a RubyForge project set up at:
119
+
120
+ http://rubyforge.org/projects/zventstools/
121
+
122
+ Anonymous SVN access:
123
+
124
+ $ svn checkout svn://rubyforge.org/var/svn/zventstools
125
+
126
+ Author: Tyler Kovacs (tyler dot kovacs at gmail dot com)
@@ -0,0 +1,138 @@
1
+ # /usr/local/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/associations/has_and_belongs_to_many_association.rb
2
+ module ActiveRecord
3
+ module Associations
4
+ class HasAndBelongsToManyAssociation
5
+ # Copies a HABTM association collection from one object to another
6
+ # without instantiating a bunch of ActiveRecord objects. This is
7
+ # faster than the standard assignment operation since:
8
+ #
9
+ # 1. Eliminates massive number of SQL calls used in standard HABTM
10
+ # copy by changing it from an O(n) operation to O(1) where
11
+ # n is the number of objects in the association collection.
12
+ # 2. It transfers only object IDs back and forth between the database
13
+ # instead of all object attributes. Resulting in less work for
14
+ # the database, less data transferred and less memory used in ruby.
15
+ # 3. It doesn't instantiate ActiveRecord objects in memory.
16
+ #
17
+ # A normal HABTM copy (e.g., person1.items = person2.items) results
18
+ # in the following SQL calls.
19
+ #
20
+ # SELECT * FROM items INNER JOIN items_people ON items.id = items_people.item_id WHERE (items_people.person_id = 1 )
21
+ # SELECT * FROM items INNER JOIN items_people ON items.id = items_people.item_id WHERE (items_people.person_id = 2 )
22
+ # DELETE FROM items_people WHERE person_id = 2 AND item_id IN (4)
23
+ # INSERT INTO items_people (`item_id`, `person_id`) VALUES (1, 2)
24
+ # INSERT INTO items_people (`item_id`, `person_id`) VALUES (2, 2)
25
+ # INSERT INTO items_people (`item_id`, `person_id`) VALUES (3, 2)
26
+ #
27
+ # Notice that:
28
+ # - items AR objects are instantiated unnecessarily (especially since
29
+ # person2.items are about to be deleted)
30
+ # - 1 SQL call is issued for each object (item) in the association
31
+ # collection (items_people)
32
+ #
33
+ # whereas person.items.fast_copy will result in the
34
+ # the following SQL calls greatly reducing the impact on the database
35
+ # and on ruby memory utilization.
36
+ #
37
+ # DELETE FROM items_people WHERE person_id = 2
38
+ # SELECT item_id FROM items_people WHERE person_id = 1
39
+ # REPLACE INTO items_people (person_id,item_id) VALUES (2,3),(2,2),(2,1)
40
+ #
41
+ # Here are some benchmarks:
42
+ #
43
+ # when n = 10 and 26 objects in e2.groups:
44
+ #
45
+ # Benchmark.bm do |x|
46
+ # x.report { for i in 1..n; e1.groups.clear;e1.groups = e2.groups;end }
47
+ # x.report { for i in 1..n; e1.groups.clear;e1.groups.fast_copy(e2);end }
48
+ # end
49
+ #
50
+ # user system total real
51
+ # 1.140000 0.040000 1.180000 ( 1.832122)
52
+ # 0.020000 0.010000 0.030000 ( 0.125368)
53
+ #
54
+ # when n = 100 and 26 objects in e2.groups:
55
+ #
56
+ # user system total real
57
+ # 11.140000 0.360000 11.500000 ( 18.171410)
58
+ # 0.140000 0.010000 0.150000 ( 2.368200)
59
+ #
60
+ #
61
+ # This method also supports HABTM join tables with additional attributes.
62
+ # Simply pass in an attribute hash as the second argument and it will
63
+ # add the attributes to the records it creates in the join table.
64
+ #
65
+ # e.g, person1.items.fast_copy(person2, {:created_at => Time.now})
66
+ #
67
+ # REALITY CHECK: The HABTM docs refer to collection_singular_ids=ids
68
+ # which implies identical functionality, but I can't find mention of
69
+ # this method in anything other than the documentation. Maybe this
70
+ # actually already exists and I'm just blind, but from the looks of
71
+ # http://dev.rubyonrails.org/ticket/2917, it appears that it is a
72
+ # documentation bug.
73
+ def fast_copy(other_object,attributes = {})
74
+ self.fast_clear
75
+ other_object_assocation_ids = other_object.send(@reflection.name).ids
76
+ return [] if other_object_assocation_ids.empty?
77
+
78
+ column_names = [ @reflection.primary_key_name,
79
+ @reflection.association_foreign_key ]
80
+ attribute_values = []
81
+ attributes.keys.each{|k|
82
+ column_names << k
83
+ attribute_values << attributes[k]
84
+ }
85
+
86
+ @owner.connection.execute("REPLACE INTO #{@reflection.options[:join_table]} (#{column_names.join(",")}) VALUES #{other_object_assocation_ids.map{|aid| "(#{@owner.quoted_id},#{aid}#{attributes.empty? ? "" : ("," + attribute_values.join(','))})"}.join(",")}")
87
+ return other_object_assocation_ids
88
+ end
89
+
90
+ # Return the list of IDs in this association collection without
91
+ # unnecessarily instantiating a bunch of Active Record objects.
92
+ def ids
93
+ if self.loaded?
94
+ self.map{|x| x.id}
95
+ else
96
+ connection.select_all("SELECT #{@reflection.association_foreign_key} FROM #{@reflection.options[:join_table]} WHERE #{@reflection.primary_key_name} = #{@owner.quoted_id}").map!{|x| x[@reflection.association_foreign_key].to_i}
97
+ end
98
+ end
99
+
100
+ # Removes all records from this association. Returns +self+ so method
101
+ # calls may be chained. If this association is not marked as :dependent,
102
+ # then use a faster delete_all method that doesn't instantiate a bunch
103
+ # of AR objects.
104
+ def fast_clear
105
+ if @reflection.options[:dependent] && @reflection.options[:dependent] == :delete_all
106
+ return self if length.zero? # forces load_target if hasn't happened already
107
+ destroy_all
108
+ else
109
+ fast_delete_all
110
+ end
111
+
112
+ self
113
+ end
114
+
115
+ def fast_delete_all
116
+ @owner.connection.execute("DELETE FROM #{@reflection.options[:join_table]} WHERE #{@reflection.primary_key_name} = #{@owner.quoted_id}")
117
+ @target = []
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ # /usr/local/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/associations/has_many_association.rb
124
+ module ActiveRecord
125
+ module Associations
126
+ # Return the list of IDs in this association without unnecessarily
127
+ # instantiating a bunch of Active Record objects.
128
+ class HasManyAssociation
129
+ def ids
130
+ if self.loaded?
131
+ self.map{|x| x.id}
132
+ else
133
+ connection.select_all("SELECT #{@reflection.klass.primary_key} FROM #{@reflection.klass.table_name} WHERE #{@reflection.primary_key_name} = #{@owner.quoted_id}").map!{|x| x[@reflection.klass.primary_key].to_i}
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,90 @@
1
+ require 'test/unit'
2
+ require File.dirname(__FILE__) + '/../../../../test/test_helper'
3
+
4
+ class Person < ActiveRecord::Base
5
+ set_table_name 'persons'
6
+ has_and_belongs_to_many :items
7
+ has_many :pets
8
+ end
9
+
10
+ class Item < ActiveRecord::Base
11
+ set_table_name 'items'
12
+ has_and_belongs_to_many :persons
13
+ end
14
+
15
+ class Pet < ActiveRecord::Base
16
+ set_table_name 'pets'
17
+ belongs_to :person
18
+ end
19
+
20
+ class AssociationToolsTest < Test::Unit::TestCase
21
+ Item.connection.execute "DROP TABLE IF EXISTS items;"
22
+ Item.connection.execute "DROP TABLE IF EXISTS persons;"
23
+ Item.connection.execute "DROP TABLE IF EXISTS items_people;"
24
+ Item.connection.execute "DROP TABLE IF EXISTS pets;"
25
+ Item.connection.execute "CREATE TABLE items (id INT NOT NULL AUTO_INCREMENT, name VARCHAR(32) DEFAULT '', PRIMARY KEY(id));"
26
+ Item.connection.execute "CREATE TABLE persons (id INT NOT NULL AUTO_INCREMENT, name VARCHAR(32) DEFAULT '', PRIMARY KEY(id));"
27
+ Item.connection.execute "CREATE TABLE items_people (item_id INT NOT NULL, person_id INT NOT NULL);"
28
+ Item.connection.execute "CREATE TABLE pets (id INT NOT NULL AUTO_INCREMENT, name VARCHAR(32) DEFAULT '', person_id INT NOT NULL, PRIMARY KEY(id));"
29
+
30
+ Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + '/fixtures'
31
+ fixtures :persons, :items, :items_people, :pets
32
+
33
+ def test_has_and_belongs_to_many_ids
34
+ bob = persons(:bob)
35
+ assert_equal 3, bob.items.length
36
+ assert_equal [1,2,3], bob.items.map{|i| i.id}.sort
37
+ assert bob.items.loaded?
38
+
39
+ assert bob.reload
40
+ assert !bob.items.loaded?
41
+ assert_equal [1,2,3], bob.items.ids.sort
42
+ assert !bob.items.loaded?
43
+ end
44
+
45
+ def test_has_many_ids
46
+ bob = persons(:bob)
47
+ assert_equal 2, bob.pets.length
48
+ assert_equal [1,2], bob.pets.map{|p| p.id}.sort
49
+ assert bob.pets.loaded?
50
+
51
+ assert bob.reload
52
+ assert !bob.pets.loaded?
53
+ assert_equal [1,2], bob.pets.ids.sort
54
+ assert !bob.pets.loaded?
55
+ end
56
+
57
+ def test_has_and_belongs_to_many_fast_delete_all
58
+ bob = persons(:bob)
59
+ assert_equal 3, bob.items.length
60
+ assert bob.reload
61
+ assert !bob.items.loaded?
62
+
63
+ bob.items.fast_delete_all
64
+ assert !bob.items.loaded?
65
+ assert bob.reload
66
+ assert_equal 0, bob.items.length
67
+ end
68
+
69
+ def test_has_and_belongs_to_many_fast_copy
70
+ bob = persons(:bob)
71
+ larry = persons(:larry)
72
+
73
+ larry.items.fast_copy(bob)
74
+ assert !bob.items.loaded?
75
+ assert !larry.items.loaded?
76
+ assert_equal [1,2,3], bob.items.map{|i| i.id}.sort
77
+ assert_equal [1,2,3], larry.items.map{|i| i.id}.sort
78
+ end
79
+
80
+ def test_has_and_belongs_to_many_standard_copy
81
+ bob = persons(:bob)
82
+ larry = persons(:larry)
83
+
84
+ larry.items = bob.items
85
+ assert bob.items.loaded?
86
+ assert larry.items.loaded?
87
+ assert_equal [1,2,3], bob.items.map{|i| i.id}.sort
88
+ assert_equal [1,2,3], larry.items.map{|i| i.id}.sort
89
+ end
90
+ end
metadata ADDED
@@ -0,0 +1,52 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.10
3
+ specification_version: 1
4
+ name: association_collection_tools
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.0.1
7
+ date: 2006-11-02
8
+ summary: Adds fast_copy method to has_and_belongs_to_many associations cloning associations much faster than the assignment operator. Adds ids methods to has_many and has_and_belongs_to_many associations for retrieving object ids without instantiating ActiveRecord objects.
9
+ require_paths:
10
+ - lib
11
+ email: tyler.kovacs@zvents.com
12
+ homepage: http://blog.zvents.com/2006/11/3/rails-plugin-association-collection-tools
13
+ rubyforge_project:
14
+ description:
15
+ autorequire: association_collection_tools
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ authors:
27
+ - Tyler Kovacs
28
+ files:
29
+ - lib/association_collection_tools.rb
30
+ - README
31
+ test_files:
32
+ - test/association_tools_test.rb
33
+ rdoc_options: []
34
+
35
+ extra_rdoc_files:
36
+ - README
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ requirements: []
42
+
43
+ dependencies:
44
+ - !ruby/object:Gem::Dependency
45
+ name: memcache-client
46
+ version_requirement:
47
+ version_requirements: !ruby/object:Gem::Version::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: 1.0.3
52
+ version: