association_collection_tools 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: