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 +126 -0
- data/lib/association_collection_tools.rb +138 -0
- data/test/association_tools_test.rb +90 -0
- metadata +52 -0
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:
|