has-many-with-set 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2012 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.textile ADDED
@@ -0,0 +1,142 @@
1
+ h1. has-many-with-set
2
+
3
+ h3. A smarter way of doing many-to-many relationships in Ruby On Rails.
4
+
5
+ h2. Introduction
6
+
7
+ Rails has two ways to model many-to-many relationships: *_has_and_belongs_to_many_* and *_has_many :through_*, this gem introduces a third one: *_has_many_with_set_*.
8
+
9
+ *_has_many_with_set_* is equivalent to *_has_and_belongs_to_many_* in functionality. It works only when you do not want information about a relationship but the relationship itself, behind the curtains though, they do not work anything alike, *_has_many_with_set_* is far more efficient in terms of data size as it reduces the redundancy that occurs in a normal many-to-many relationship when the cardinality is low, that is, the same combination occurs many times. For example, in a blog application, when many posts share the same tags.
10
+
11
+ h2. How so?
12
+
13
+ The regular way of doing many-to-many relationships is using a join table to relate two tables, both ways of doing it in Ruby On Rails use this method, the only difference is the degree of control they give you on the "intermediary" table, one hides it from you (which is nice) and the other allows you to put more data in it besides the relationship, use validations, callbacks, etc.
14
+
15
+ The _join_ table model is a very redundant way of storing these relationships if the same combination happens more than once because you have to create the same amount rows in the join table each time you save this combination for each different _parent_.
16
+
17
+ For example:
18
+
19
+ bc.. Tag.new(:name => 'programming').save
20
+ Tag.new(:name => 'open source').save
21
+ Tag.new(:name => 'startups').save
22
+ Tag.new(:name => 'ruby').save
23
+ Tag.new(:name => 'development').save
24
+
25
+ tags = Tag.all
26
+
27
+ 1000.times do
28
+ a = Article.new(:title => "Buzzword about buzzwords!",
29
+ :body => "Lorem ipsum")
30
+
31
+ rand(tags.size + 1).times do
32
+ t = tags[rand(tags.size)]
33
+ a.tags << t
34
+ end
35
+
36
+ a.tags.uniq!
37
+ a.save
38
+ end
39
+
40
+ ArticlesTags = Class.new(ActiveRecord::Base)
41
+ ArticlesTags.count # this class doesn't exist by default,
42
+ # I had to create it by hand for the example.
43
+ => 1932
44
+
45
+ p. So we create five tags, and we create 1000 articles with a random combination of tags, not surprisingly, our join table has plenty of rows to represent all the relationships between our articles and their tags, if this were to behave linearly, if we had 1,000,000 articles we would have 1,932,000 rows just to represent the relationship.
46
+
47
+ This example (albeit a bit unrealistic) shows how redundant this is, even though we are using the same combination of tags over and over again we get more and more rows, if we are speaking about thousands it is not a big problem but when your databases grow to the hundreds of thousands or the millions, stuff like this starts to matter.
48
+
49
+ This is what this gem fixes, it makes sure that when you create a combination of items it is unique and it gets used as many times as its needed when requested again, like a *set*.
50
+
51
+ *_has-many-with-set_* is here to help.
52
+
53
+ h2. Installation
54
+
55
+ *Rails 3.x*
56
+
57
+ To use it, add it to your Gemfile:
58
+
59
+ @gem 'has-many-with-set'@
60
+
61
+ That's pretty much it!
62
+
63
+ h2. Usage
64
+
65
+ To to use *_has-many-with-set_* to relate two already existing models you have to create the underlying tables that are going to be used by it, this is very easily done by generating a migration for them:
66
+
67
+ @rails generate has_many_with_set:migration PARENT CHILD@
68
+
69
+ And add the relationship to your parent model:
70
+
71
+ bc.. class Parent < ActiveRecord::Base
72
+ has_many_with_set :children
73
+ end
74
+
75
+ p. And that's it! You can start using it in your application. This can be done for as many models as you want, (you have to create migrations for all combinations!) you can even use multiple sets to relate different data to the same parent model (like Authors and Tags for your Articles).
76
+
77
+ h2. Example
78
+
79
+ Using our previous example:
80
+
81
+ bc. rails g has_many_with_set:migration Article Tag
82
+ create db/migrate/20121106063326_create_articles_tags_set.rb
83
+
84
+ bc.. class Article < ActiveRecord::Base
85
+ attr_accessible :body, :title
86
+
87
+ has_many_with_set :tags # <--- key part!
88
+ end
89
+
90
+ Tag.new(:name => 'programming').save
91
+ Tag.new(:name => 'open source').save
92
+ Tag.new(:name => 'startups').save
93
+ Tag.new(:name => 'ruby').save
94
+ Tag.new(:name => 'development').save
95
+
96
+ tags = Tag.all
97
+
98
+ 1000.times do
99
+ a = Article.new(:title => "Buzzword about buzzwords!",
100
+ :body => "Lorem ipsum")
101
+
102
+ rand(tags.size + 1).times do
103
+ t = tags[rand(tags.size)]
104
+ a.tags << t
105
+ end
106
+
107
+ a.tags.uniq!
108
+ a.save
109
+ end
110
+
111
+ ArticlesTagsSetsTag = Class.new(ActiveRecord::Base)
112
+ ArticlesTagsSetsTag.count # this class doesn't exist by default,
113
+ # I had to create it by hand for the example.
114
+ => 80
115
+
116
+ Article.first.tags
117
+ => [#<Tag id: 1, name: "programming", ...>]
118
+
119
+ Article.last.tags
120
+ => [#<Tag id: 1, name: "programming", ...>, #<Tag id: 5, name: "development", ...]
121
+
122
+ p. Same example as before, just now using *_has_many_with_set_*. We get the impressive number of 80 rows to represent the same information that we had before with thousands of rows (roughly the same, since we use random combinations is not _exactly_ the same article/tag layout).
123
+
124
+ The funny thing in this particular example, is that since we have only five tags, there are only 32 possible ways to combine five tags together, these 32 combinations amount to 80 rows in our relationship table.... that is, even if we had a million articles we would still have the same 80 rows to represent our relationships, we don't need to create any more rows!!
125
+
126
+ h2. Final remarks
127
+
128
+ If you want to read a detailed post about how this works and what this gem does with some useful diagrams and sample queries, you can check out this blog post : "Using sets for many-to-many-relationships":http://ebobby.org/2012/11/11/Using-Sets-For-Many-To-Many-Relationships.html.
129
+
130
+ Please keep in mind that *_has-many-with-set_* is not without some caveats:
131
+
132
+ * It can only be used when you do not need to put extra information in the relationships rows since they are shared among many parents.
133
+ * It is only effective when there is a high natural redundancy in your data, that is, when many sets can be shared among many parents.
134
+ * Although the retrieval queries are the same as with regular *_has_and_belongs_to_many_* and have no extra cost, it does have a tiny bit of extra cost when saving or updating since we have to find or create a suitable set before actually saving the parent record to the database. This cost is probably negligible as opposed to writing all the time, but I can't say it's free.
135
+
136
+ This is one humble attempt to help make Ruby On Rails a bit more useful with large data sets and applications, I hope you enjoy it and is useful to you, please email me with comments or suggestions (or even code!).
137
+
138
+ h2. Author
139
+
140
+ * Francisco Soto <ebobby@ebobby.org>
141
+
142
+ Copyright © 2012 Francisco Soto (http://ebobby.org) released under the MIT license.
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+
8
+ Bundler::GemHelper.install_tasks
9
+
10
+ require 'rake/testtask'
11
+
12
+ Rake::TestTask.new(:test) do |t|
13
+ t.libs << 'lib'
14
+ t.libs << 'test'
15
+ t.pattern = 'test/**/*_test.rb'
16
+ t.verbose = false
17
+ end
18
+
19
+ task :default => :test
@@ -0,0 +1,32 @@
1
+ require 'rails/generators'
2
+
3
+ module HasManyWithSet
4
+ class MigrationGenerator < Rails::Generators::Base
5
+ include Rails::Generators::Migration
6
+ source_root File.expand_path('../templates', __FILE__)
7
+
8
+ def self.next_migration_number(path)
9
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
10
+ end
11
+
12
+ ORM_IDENTIFIER_SIZE_LIMIT = 63
13
+
14
+ argument :parent, :type => :string, :required => true
15
+ argument :child, :type => :string, :required => true
16
+
17
+ attr_accessor :parent, :child
18
+
19
+ desc "This generates the migration file needed for a has_many_with_set relationship."
20
+ def create_migration
21
+ @parent_table = parent.tableize
22
+ @child_table = child.tableize
23
+ @set_table = "#{ @parent_table }_#{ @child_table }_sets"
24
+ @set_items_table = "#{ @set_table }_#{ @child_table }"
25
+ @migration_class_name = "create_#{ @set_table }".classify
26
+ @items_table_set_table_index = "ix_items_#{ @set_items_table }"[0, ORM_IDENTIFIER_SIZE_LIMIT]
27
+ @items_table_child_table_index = "ix_#{ @child_table }_#{ @set_items_table }"[0, ORM_IDENTIFIER_SIZE_LIMIT]
28
+
29
+ migration_template "sets.rb.erb", "db/migrate/#{ @migration_class_name.tableize.singularize }.rb"
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,18 @@
1
+ class <%= @migration_class_name %> < ActiveRecord::Migration
2
+ def change
3
+ create_table :<%= @set_table %> do |t|
4
+ t.timestamps
5
+ end
6
+
7
+ create_table :<%= @set_items_table %> do |t|
8
+ t.references :<%= @set_table.singularize %>, :null => false
9
+ t.references :<%= @child_table.singularize %>, :null => false
10
+ end
11
+
12
+ add_column :<%= @parent_table %>, :<%= @set_table.singularize %>_id, :integer
13
+
14
+ add_index :<%= @parent_table %>, :<%= @set_table.singularize %>_id
15
+ add_index :<%= @set_items_table %>, :<%= @set_table.singularize %>_id, :name => :<%= @items_table_set_table_index %>
16
+ add_index :<%= @set_items_table %>, :<%= @child_table.singularize %>_id, :name => :<%= @items_table_child_table_index %>
17
+ end
18
+ end
@@ -0,0 +1,45 @@
1
+ module HasManyWithSet
2
+ module Accessors
3
+ def self.build_loader_method (child_table_name, set_table_name)
4
+ set_table_id = "#{ set_table_name.singularize }_id"
5
+ set_table_name_singular = set_table_name.singularize
6
+
7
+ Proc.new {
8
+ values = []
9
+ values = send(set_table_name_singular).send(child_table_name).to_a unless send(set_table_id).nil?
10
+ values
11
+ }
12
+ end
13
+
14
+ def self.build_getter_method (instance_var_name, loader_method_name)
15
+ Proc.new {
16
+ values = instance_variable_get(instance_var_name)
17
+
18
+ unless values
19
+ values = send(loader_method_name)
20
+ instance_variable_set(instance_var_name, values)
21
+ end
22
+
23
+ values
24
+ }
25
+ end
26
+
27
+ def self.build_setter_method (instance_var_name)
28
+ Proc.new { |elements|
29
+ elements = [] if elements.nil?
30
+
31
+ unless elements.is_a? Array
32
+ if elements.respond_to?(:is_a)
33
+ elements = elements.to_a
34
+ else
35
+ elements = [ elements ]
36
+ end
37
+ end
38
+
39
+ elements = elements.flatten.uniq
40
+
41
+ instance_variable_set(instance_var_name, elements)
42
+ }
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,50 @@
1
+ module HasManyWithSet
2
+ module Callbacks
3
+ def self.build_saver_callback (set_table_name, set_items_table_name,
4
+ child_table_name, instance_var_name)
5
+ empty_set_query = Queries.build_find_empty_set_query(set_table_name, set_items_table_name)
6
+ find_set_query = Queries.build_find_set_query(set_table_name, set_items_table_name, child_table_name)
7
+
8
+ set_item_id_setter = "#{ set_table_name.singularize }_id="
9
+ set_items_setter = "#{ child_table_name }="
10
+
11
+ klass = Object.const_get(set_table_name.classify)
12
+
13
+ Proc.new {
14
+ set = nil
15
+ values = send(child_table_name)
16
+
17
+ if values.blank?
18
+ ActiveRecord::Base.transaction do
19
+ set = klass.find_by_sql(empty_set_query).first
20
+
21
+ if set.nil?
22
+ set = klass.new
23
+ set.save
24
+ end
25
+ end
26
+ else
27
+ values = values.flatten.uniq
28
+
29
+ values.each do |v| v.save if v.changed? end
30
+
31
+ ActiveRecord::Base.transaction do
32
+ set = klass.find_by_sql([ find_set_query,
33
+ values.map { |v| v.id },
34
+ values.size,
35
+ values.size ]).first
36
+ if set.nil?
37
+ set = klass.new
38
+ set.send(set_items_setter, values)
39
+ set.save
40
+ end
41
+
42
+ send(set_items_setter, values)
43
+ end
44
+ end
45
+
46
+ send(set_item_id_setter, set.id)
47
+ }
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,44 @@
1
+ module HasManyWithSet
2
+ # This is the magic entry point method that adds set relationships to a model.
3
+ def has_many_with_set (child)
4
+ build_set_relationship self.to_s, child.to_s.classify
5
+ end
6
+
7
+ private
8
+
9
+ def build_set_relationship (parent_model_name, child_model_name)
10
+ parent_table_name = parent_model_name.tableize
11
+ child_table_name = child_model_name.tableize
12
+ set_table_name = "#{ parent_table_name }_#{ child_table_name }_sets"
13
+ set_model_name = set_table_name.classify
14
+ set_items_table_name = "#{ set_table_name }_#{ child_table_name }"
15
+ instance_var_name = "@#{ child_table_name }"
16
+ setter_method_name = "#{ child_table_name }="
17
+ loader_method_name = "#{ set_items_table_name }_loader"
18
+ save_callback_method_name = "#{ set_items_table_name }_save_callback"
19
+
20
+ Relationships.create_set_model(set_model_name)
21
+ Relationships.relate_child_to_set(set_model_name, child_model_name)
22
+ Relationships.relate_parent_to_set(set_model_name, parent_model_name)
23
+
24
+ define_method(loader_method_name,
25
+ Accessors.build_loader_method(child_table_name, set_table_name))
26
+
27
+ define_method(child_table_name,
28
+ Accessors.build_getter_method(instance_var_name, loader_method_name))
29
+
30
+ define_method(setter_method_name,
31
+ Accessors.build_setter_method(instance_var_name))
32
+
33
+ define_method(save_callback_method_name,
34
+ Callbacks.build_saver_callback(set_table_name, set_items_table_name,
35
+ child_table_name, instance_var_name))
36
+
37
+ class_eval do
38
+ private loader_method_name
39
+ private save_callback_method_name
40
+
41
+ before_save save_callback_method_name
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,22 @@
1
+ module HasManyWithSet
2
+ module Queries
3
+ def self.build_find_empty_set_query(set_table_name, set_items_table_name)
4
+ "select #{ set_table_name }.id from #{ set_table_name }
5
+ where not exists (select null from #{ set_items_table_name }
6
+ where #{ set_items_table_name }.#{ set_table_name.singularize }_id = #{ set_table_name }.id)"
7
+ end
8
+
9
+ def self.build_find_set_query(set_table_name, set_items_table_name, child_table_name)
10
+ "select #{ set_table_name }.id from #{ set_items_table_name }
11
+ join #{ set_table_name } on
12
+ #{ set_table_name }.id = #{ set_items_table_name }.#{ set_table_name.singularize }_id
13
+ where
14
+ #{set_items_table_name }.#{ child_table_name.singularize }_id IN (?)
15
+ and (select count(*) from #{ set_items_table_name } c
16
+ where c.#{ set_table_name.singularize }_id =
17
+ #{ set_items_table_name }.#{ set_table_name.singularize }_id) = ?
18
+ group by #{ set_table_name }.id
19
+ having count(*) = ?"
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ module HasManyWithSet
2
+ module Relationships
3
+ def self.create_set_model (set_model_name)
4
+ Object.const_set(set_model_name, Class.new(ActiveRecord::Base)) unless
5
+ Object.const_defined?(set_model_name) # this *should* never happen...
6
+ end
7
+
8
+ def self.relate_child_to_set (set_model_name, child_model_name)
9
+ # Take the child model and add a regular many-to-many relationship to the Set model...
10
+ Object.const_get(child_model_name).class_eval do
11
+ has_and_belongs_to_many set_model_name.tableize.to_sym
12
+ end
13
+
14
+ # ... and take the Set model and finish the many-to-many relationship.
15
+ Object.const_get(set_model_name).class_eval do
16
+ has_and_belongs_to_many child_model_name.tableize.to_sym
17
+ end
18
+ end
19
+
20
+ def self.relate_parent_to_set (set_model_name, parent_model_name)
21
+ # The parent object has a FK to the Set table, so it belongs_to it.
22
+ Object.const_get(parent_model_name).class_eval do
23
+ belongs_to set_model_name.tableize.singularize.to_sym
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ module HasManyWithSet
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,19 @@
1
+ require "active_record"
2
+ require "active_support"
3
+
4
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
5
+
6
+ require "has-many-with-set/has-many-with-set"
7
+ require "has-many-with-set/relationships"
8
+ require "has-many-with-set/queries"
9
+ require "has-many-with-set/accessors"
10
+ require "has-many-with-set/callbacks"
11
+
12
+ module HasManyWithSet
13
+ end
14
+
15
+ if defined?(ActiveRecord::Base)
16
+ ActiveRecord::Base.extend HasManyWithSet
17
+ end
18
+
19
+ $LOAD_PATH.shift
@@ -0,0 +1,119 @@
1
+ require "test_helper"
2
+
3
+ PARENT = "ModelOne"
4
+ CHILD = "ModelTwo"
5
+ MIGRATION_PATH = "test/tmp/"
6
+ MIGRATION_FILE = "db/migrate/create_model_ones_model_twos_set"
7
+
8
+ class MigrationGeneratorTest < Rails::Generators::TestCase
9
+ tests HasManyWithSet::MigrationGenerator
10
+
11
+ destination MIGRATION_PATH
12
+ setup :prepare_destination
13
+
14
+ test "Generate migration" do
15
+ run_generator [ PARENT, CHILD ]
16
+
17
+ assert_migration MIGRATION_FILE
18
+ end
19
+ end
20
+
21
+ # Migration test has to run first, I do not like that but is the only way to actually
22
+ # test the whole thing.
23
+ PrepareActiveRecord.prepare_default_schema
24
+ PrepareActiveRecord.run_migration(MIGRATION_FILE, MIGRATION_PATH)
25
+
26
+ class HasManyWithSetTest < ActiveSupport::TestCase
27
+ test "parent class has the getter" do
28
+ assert_respond_to ModelOne.new, "model_twos"
29
+ end
30
+
31
+ test "parent class has the setter" do
32
+ assert_respond_to ModelOne.new, "model_twos="
33
+ end
34
+
35
+ test "getter type" do
36
+ assert_kind_of Array, ModelOne.new.model_twos
37
+ end
38
+
39
+ test "children can be saved" do
40
+ 15.times do
41
+ assert ModelTwo.new.save
42
+ end
43
+
44
+ assert (ModelTwo.all.size == 15)
45
+ end
46
+
47
+ test "parent saved with empty set" do
48
+ assert ModelOne.new(:num => 0).save
49
+ assert ModelOne.last.model_twos.size == 0
50
+ end
51
+
52
+ test "parent saved with non-empty set" do
53
+ record = ModelOne.new
54
+ record.model_twos = ModelTwo.all
55
+ record.num = record.model_twos.size
56
+ assert record.save
57
+
58
+ record = ModelOne.find(record.id)
59
+ assert record.num == record.model_twos.size
60
+ end
61
+
62
+ test "parent saved with several children" do
63
+ ModelTwo.all.each do |m|
64
+ record = ModelOne.new(:num => 1)
65
+ record.model_twos << m
66
+ assert record.save
67
+
68
+ record = ModelOne.find(record.id)
69
+ assert record.num == record.model_twos.size
70
+ end
71
+ end
72
+
73
+ test "set reuse" do
74
+ items = ModelTwo.all
75
+
76
+ 25.times do
77
+ master_record = ModelOne.new
78
+
79
+ rand(items.size + 1).times do
80
+ master_record.model_twos << items[rand(items.size)]
81
+ end
82
+
83
+ master_record.num = master_record.model_twos.size
84
+ master_record.save
85
+
86
+ master_items = master_record.model_twos
87
+
88
+ set_id = master_record.send("model_ones_model_twos_set_id")
89
+
90
+ 100.times do
91
+ record = ModelOne.new(:num => master_items.size)
92
+ record.model_twos = master_items
93
+ record.save
94
+
95
+ assert(record.send("model_ones_model_twos_set_id") == set_id,
96
+ "Set ids do not match #{ record.send('model_ones_model_twos_set_id') } == #{ set_id }")
97
+ end
98
+ end
99
+ end
100
+
101
+ test "do not save repeated items" do
102
+ items = ModelTwo.all
103
+ master_record = ModelOne.new(:num => items.size)
104
+
105
+ master_record.model_twos = ModelTwo.all
106
+ master_record.model_twos << items
107
+ master_record.model_twos << items
108
+ master_record.model_twos << items
109
+ master_record.save
110
+
111
+ assert master_record.model_twos.size == items.size
112
+ end
113
+
114
+ test "items count match" do
115
+ ModelOne.all.each do |m|
116
+ assert m.num == m.model_twos.size
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,7 @@
1
+ class ModelTwo < ActiveRecord::Base
2
+ end
3
+
4
+ class ModelOne < ActiveRecord::Base
5
+ attr_accessible :num
6
+ has_many_with_set :model_two
7
+ end
@@ -0,0 +1,39 @@
1
+ class PrepareActiveRecord
2
+ class << self
3
+ def prepare_default_schema
4
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
5
+
6
+ ActiveRecord::Schema.define do
7
+ self.verbose = false
8
+
9
+ create_table :model_ones do |t|
10
+ t.integer :num
11
+ t.timestamps
12
+ end
13
+
14
+ create_table :model_twos do |t|
15
+ t.timestamps
16
+ end
17
+ end
18
+ end
19
+
20
+ def run_migration(relative, root)
21
+ file = migration_file_name(relative, root)
22
+
23
+ require(file)
24
+
25
+ # I think this is horrendous but I don't want to hardcode the name of the class either.
26
+ loaded_klass = Module.constants.last
27
+
28
+ Object.const_get(loaded_klass).new.change
29
+ end
30
+
31
+ private
32
+
33
+ def migration_file_name(relative, root)
34
+ absolute = File.expand_path(relative, root)
35
+ dirname, file_name = File.dirname(absolute), File.basename(absolute).sub(/\.rb$/, '')
36
+ Dir.glob("#{dirname}/[0-9]*_*.rb").grep(/\d+_#{file_name}.rb$/).first
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,13 @@
1
+ require "active_record"
2
+ require "active_support"
3
+ require "rails"
4
+ require "rails/generators"
5
+ require "rails/test_help"
6
+
7
+ require File.expand_path("lib/has-many-with-set")
8
+ require "generators/has_many_with_set/migration_generator"
9
+
10
+ Rails.backtrace_cleaner.remove_silencers!
11
+
12
+ # Load support files
13
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
@@ -0,0 +1,18 @@
1
+ class CreateModelOnesModelTwosSet < ActiveRecord::Migration
2
+ def change
3
+ create_table :model_ones_model_twos_sets do |t|
4
+ t.timestamps
5
+ end
6
+
7
+ create_table :model_ones_model_twos_sets_model_twos do |t|
8
+ t.references :model_ones_model_twos_set, :null => false
9
+ t.references :model_two, :null => false
10
+ end
11
+
12
+ add_column :model_ones, :model_ones_model_twos_set_id, :integer
13
+
14
+ add_index :model_ones, :model_ones_model_twos_set_id
15
+ add_index :model_ones_model_twos_sets_model_twos, :model_ones_model_twos_set_id, :name => :ix_items_model_ones_model_twos_sets_model_twos
16
+ add_index :model_ones_model_twos_sets_model_twos, :model_two_id, :name => :ix_model_twos_model_ones_model_twos_sets_model_twos
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: has-many-with-set
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Francisco Soto
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-13 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 3.2.8
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 3.2.8
30
+ - !ruby/object:Gem::Dependency
31
+ name: sqlite3
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: A smarter way of doing many-to-many relationships in Rails.
47
+ email:
48
+ - ebobby@ebobby.org
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - lib/generators/has_many_with_set/migration_generator.rb
54
+ - lib/generators/has_many_with_set/templates/sets.rb.erb
55
+ - lib/has-many-with-set/accessors.rb
56
+ - lib/has-many-with-set/callbacks.rb
57
+ - lib/has-many-with-set/has-many-with-set.rb
58
+ - lib/has-many-with-set/queries.rb
59
+ - lib/has-many-with-set/relationships.rb
60
+ - lib/has-many-with-set/version.rb
61
+ - lib/has-many-with-set.rb
62
+ - MIT-LICENSE
63
+ - Rakefile
64
+ - README.textile
65
+ - test/has-many-with-set_test.rb
66
+ - test/support/models.rb
67
+ - test/support/prepare_activerecord.rb
68
+ - test/test_helper.rb
69
+ - test/tmp/db/migrate/20121113022938_create_model_ones_model_twos_set.rb
70
+ homepage: https://github.com/ebobby/has-many-with-set
71
+ licenses: []
72
+ post_install_message:
73
+ rdoc_options: []
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ! '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ segments:
83
+ - 0
84
+ hash: 2377775004769566141
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ! '>='
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ segments:
92
+ - 0
93
+ hash: 2377775004769566141
94
+ requirements: []
95
+ rubyforge_project:
96
+ rubygems_version: 1.8.24
97
+ signing_key:
98
+ specification_version: 3
99
+ summary: A smarter way of doing many-to-many relationships in Rails.
100
+ test_files:
101
+ - test/has-many-with-set_test.rb
102
+ - test/support/models.rb
103
+ - test/support/prepare_activerecord.rb
104
+ - test/test_helper.rb
105
+ - test/tmp/db/migrate/20121113022938_create_model_ones_model_twos_set.rb