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 +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:
|