has_metadata 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +26 -0
- data/.rspec +1 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +103 -0
- data/LICENSE +20 -0
- data/README.textile +105 -0
- data/Rakefile +36 -0
- data/VERSION +1 -0
- data/has_metadata.gemspec +63 -0
- data/lib/has_metadata.rb +118 -0
- data/lib/metadata_generator.rb +23 -0
- data/spec/has_metadata_spec.rb +125 -0
- data/spec/metadata_spec.rb +13 -0
- data/spec/spec_helper.rb +23 -0
- data/templates/create_metadata.rb +11 -0
- data/templates/metadata.rb +42 -0
- metadata +97 -0
data/.document
ADDED
data/.gitignore
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
## MAC OS
|
2
|
+
.DS_Store
|
3
|
+
|
4
|
+
## TEXTMATE
|
5
|
+
*.tmproj
|
6
|
+
tmtags
|
7
|
+
|
8
|
+
## EMACS
|
9
|
+
*~
|
10
|
+
\#*
|
11
|
+
.\#*
|
12
|
+
|
13
|
+
## VIM
|
14
|
+
*.swp
|
15
|
+
|
16
|
+
## PROJECT::GENERAL
|
17
|
+
coverage
|
18
|
+
rdoc
|
19
|
+
pkg
|
20
|
+
.bundle
|
21
|
+
.rvmrc
|
22
|
+
test.sqlite
|
23
|
+
|
24
|
+
## PROJECT::DOCUMENTATION
|
25
|
+
.yardoc
|
26
|
+
doc
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
-cfs
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
RedCloth (4.2.3)
|
5
|
+
abstract (1.0.0)
|
6
|
+
actionmailer (3.0.1)
|
7
|
+
actionpack (= 3.0.1)
|
8
|
+
mail (~> 2.2.5)
|
9
|
+
actionpack (3.0.1)
|
10
|
+
activemodel (= 3.0.1)
|
11
|
+
activesupport (= 3.0.1)
|
12
|
+
builder (~> 2.1.2)
|
13
|
+
erubis (~> 2.6.6)
|
14
|
+
i18n (~> 0.4.1)
|
15
|
+
rack (~> 1.2.1)
|
16
|
+
rack-mount (~> 0.6.12)
|
17
|
+
rack-test (~> 0.5.4)
|
18
|
+
tzinfo (~> 0.3.23)
|
19
|
+
activemodel (3.0.1)
|
20
|
+
activesupport (= 3.0.1)
|
21
|
+
builder (~> 2.1.2)
|
22
|
+
i18n (~> 0.4.1)
|
23
|
+
activerecord (3.0.1)
|
24
|
+
activemodel (= 3.0.1)
|
25
|
+
activesupport (= 3.0.1)
|
26
|
+
arel (~> 1.0.0)
|
27
|
+
tzinfo (~> 0.3.23)
|
28
|
+
activeresource (3.0.1)
|
29
|
+
activemodel (= 3.0.1)
|
30
|
+
activesupport (= 3.0.1)
|
31
|
+
activesupport (3.0.1)
|
32
|
+
arel (1.0.1)
|
33
|
+
activesupport (~> 3.0.0)
|
34
|
+
builder (2.1.2)
|
35
|
+
diff-lcs (1.1.2)
|
36
|
+
erubis (2.6.6)
|
37
|
+
abstract (>= 1.0.0)
|
38
|
+
ffi (0.6.3)
|
39
|
+
rake (>= 0.8.7)
|
40
|
+
gemcutter (0.6.1)
|
41
|
+
git (1.2.5)
|
42
|
+
i18n (0.4.2)
|
43
|
+
jeweler (1.4.0)
|
44
|
+
gemcutter (>= 0.1.0)
|
45
|
+
git (>= 1.2.5)
|
46
|
+
rubyforge (>= 2.0.0)
|
47
|
+
json_pure (1.4.6)
|
48
|
+
mail (2.2.9)
|
49
|
+
activesupport (>= 2.3.6)
|
50
|
+
i18n (~> 0.4.1)
|
51
|
+
mime-types (~> 1.16)
|
52
|
+
treetop (~> 1.4.8)
|
53
|
+
mime-types (1.16)
|
54
|
+
polyglot (0.3.1)
|
55
|
+
rack (1.2.1)
|
56
|
+
rack-mount (0.6.13)
|
57
|
+
rack (>= 1.0.0)
|
58
|
+
rack-test (0.5.6)
|
59
|
+
rack (>= 1.0)
|
60
|
+
rails (3.0.1)
|
61
|
+
actionmailer (= 3.0.1)
|
62
|
+
actionpack (= 3.0.1)
|
63
|
+
activerecord (= 3.0.1)
|
64
|
+
activeresource (= 3.0.1)
|
65
|
+
activesupport (= 3.0.1)
|
66
|
+
bundler (~> 1.0.0)
|
67
|
+
railties (= 3.0.1)
|
68
|
+
railties (3.0.1)
|
69
|
+
actionpack (= 3.0.1)
|
70
|
+
activesupport (= 3.0.1)
|
71
|
+
rake (>= 0.8.4)
|
72
|
+
thor (~> 0.14.0)
|
73
|
+
rake (0.8.7)
|
74
|
+
rspec (2.0.1)
|
75
|
+
rspec-core (~> 2.0.1)
|
76
|
+
rspec-expectations (~> 2.0.1)
|
77
|
+
rspec-mocks (~> 2.0.1)
|
78
|
+
rspec-core (2.0.1)
|
79
|
+
rspec-expectations (2.0.1)
|
80
|
+
diff-lcs (>= 1.1.2)
|
81
|
+
rspec-mocks (2.0.1)
|
82
|
+
rspec-core (~> 2.0.1)
|
83
|
+
rspec-expectations (~> 2.0.1)
|
84
|
+
rubyforge (2.0.4)
|
85
|
+
json_pure (>= 1.1.7)
|
86
|
+
sqlite3 (0.1.1)
|
87
|
+
ffi (>= 0.6.3)
|
88
|
+
thor (0.14.3)
|
89
|
+
treetop (1.4.8)
|
90
|
+
polyglot (>= 0.3.1)
|
91
|
+
tzinfo (0.3.23)
|
92
|
+
yard (0.6.1)
|
93
|
+
|
94
|
+
PLATFORMS
|
95
|
+
ruby
|
96
|
+
|
97
|
+
DEPENDENCIES
|
98
|
+
RedCloth
|
99
|
+
jeweler
|
100
|
+
rails (>= 3.0)
|
101
|
+
rspec
|
102
|
+
sqlite3
|
103
|
+
yard
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Tim Morgan
|
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,105 @@
|
|
1
|
+
h1. has_metadata -- Keep your tables narrow
|
2
|
+
|
3
|
+
| *Author* | Tim Morgan |
|
4
|
+
| *Version* | 1.0 (Oct 29, 2010) |
|
5
|
+
| *License* | Released under the MIT License. |
|
6
|
+
|
7
|
+
h2. About
|
8
|
+
|
9
|
+
Wide tables are a problem for big databases. If your @ActiveRecord@ models have
|
10
|
+
10, maybe 15 columns, some of which are @VARCHARs@ or maybe even @TEXTs@, it's
|
11
|
+
going to slow your queries down when you start to scale up.
|
12
|
+
|
13
|
+
The easy solution to this problem is to limit your projections; in other words,
|
14
|
+
to only @SELECT@ the columns that you actually need. If you've got a @users@
|
15
|
+
table with a giant @about_me@ text column, and you're only trying to look up the
|
16
|
+
user's login, then just select the @login@ column.
|
17
|
+
|
18
|
+
In the long run, though, a superior solution is to just move those
|
19
|
+
@about_me@-type columns to a completely different table. This table has just one
|
20
|
+
JSON-serialized field, making it schemaless, so it doesn't waste space. Each row
|
21
|
+
in this table is associated with a record in another table (@Metadata@ @has_one@
|
22
|
+
of your models).
|
23
|
+
|
24
|
+
This way, when your website gets huge, all of your giant, freeform data is in
|
25
|
+
one table that you can shard, or move off to an alternate database, or even a
|
26
|
+
NoSQL-type document store, or otherwise manage as you please. Your relational
|
27
|
+
tables remain slim and efficient, containing only columns that a) are indexed,
|
28
|
+
or b) you need frequent access to.
|
29
|
+
|
30
|
+
This gem includes a generator that creates the @Metadata@ model, and a module
|
31
|
+
that you can include in your models to define which fields have been spun off to
|
32
|
+
the metadata record.
|
33
|
+
|
34
|
+
h2. Installation
|
35
|
+
|
36
|
+
*Important Note:* This gem is only compatible with Ruby 1.9 and Rails 3.0.
|
37
|
+
|
38
|
+
Firstly, add the gem to your Rails project's @Gemfile@:
|
39
|
+
|
40
|
+
<pre><code>
|
41
|
+
gem 'has_metadata'
|
42
|
+
</code></pre>
|
43
|
+
|
44
|
+
Next, run the generator, which will add the @Metadata@ model and its migration
|
45
|
+
to your application.
|
46
|
+
|
47
|
+
<pre><code>
|
48
|
+
rails generate metadata
|
49
|
+
</code></pre>
|
50
|
+
|
51
|
+
h2. Usage
|
52
|
+
|
53
|
+
The first thing to think about is what columns to keep in your model. You will
|
54
|
+
need to keep any indexed columns, or any columns you perform lookups or other
|
55
|
+
SQL queries with. You should also keep any frequently accessed columns,
|
56
|
+
especially if they are small (integers or booleans). Good candidates for the
|
57
|
+
metadata table are the @TEXT@- and @VARCHAR@-type columns that you only need to
|
58
|
+
render a page or two in your app.
|
59
|
+
|
60
|
+
You'll need to change your model's schema so that it has a @metadata_id@ column
|
61
|
+
that will associate the model with its @Metadata@ instance:
|
62
|
+
|
63
|
+
<pre><code>
|
64
|
+
t.belongs_to :metadata
|
65
|
+
</code></pre>
|
66
|
+
|
67
|
+
Next, include the @HasMetadata@ module in your model, and call the
|
68
|
+
@has_metadata@ method to define the schema of your metadata. You can get more
|
69
|
+
information in the @has_metadata@ documentation, but for starters, here's a
|
70
|
+
basic example:
|
71
|
+
|
72
|
+
<pre><code>
|
73
|
+
class User < ActiveRecord::Base
|
74
|
+
include HasMetadata
|
75
|
+
has_metadata({
|
76
|
+
about_me: { type: String, length: { maximum: 512 } },
|
77
|
+
birthdate: { type: Date, presence: true },
|
78
|
+
zipcode: { type: Number, numericality: { greater_than: 9999, less_than: 10_000_} }
|
79
|
+
})
|
80
|
+
end
|
81
|
+
</code></pre>
|
82
|
+
|
83
|
+
As you can see, you pass field names mapped to a hash. The hash describes the
|
84
|
+
validation that will be performed, and is in the same format as a call to
|
85
|
+
@validates@. In addition to the @EachValidator@ keys shown above, you can also
|
86
|
+
pass a @type@ key, to constrain the Ruby type that can be assigned to the field.
|
87
|
+
|
88
|
+
Each of these fields (in this case, @about_me@, @birthdate@, and @zipcode@) can
|
89
|
+
be accessed and set as first_level methods on an instance of your model:
|
90
|
+
|
91
|
+
<pre><code>
|
92
|
+
user.about_me #=> "I was born in 1982 in Aberdeen. My father was a carpenter from..."
|
93
|
+
</code></pre>
|
94
|
+
|
95
|
+
... and thus, used as part of @form_for@ fields:
|
96
|
+
|
97
|
+
<pre><code>
|
98
|
+
form_for user do |f|
|
99
|
+
f.text_area :about_me, rows: 5, cols: 80
|
100
|
+
end
|
101
|
+
</code></pre>
|
102
|
+
|
103
|
+
The only thing you _can't_ do is use these fields in a query, obviously. You
|
104
|
+
can't do something like @User.where(zipcode: 90210)@, because that column
|
105
|
+
doesn't exist on the @users@ table.
|
data/Rakefile
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'rake'
|
2
|
+
begin
|
3
|
+
require 'bundler'
|
4
|
+
rescue LoadError
|
5
|
+
puts "Bundler is not installed; install with `gem install bundler`."
|
6
|
+
exit 1
|
7
|
+
end
|
8
|
+
|
9
|
+
Bundler.require :default
|
10
|
+
|
11
|
+
Jeweler::Tasks.new do |gem|
|
12
|
+
gem.name = "has_metadata"
|
13
|
+
gem.summary = %Q{Reduce your table width by moving non-indexed columns to a separate metadata table}
|
14
|
+
gem.description = %Q{has_metadata lets you move non-indexed and weighty columns off of your big tables by creating a separate metadata table to store all this extra information. Works with Ruby 1.9. and Rails 3.0.}
|
15
|
+
gem.email = "git@timothymorgan.info"
|
16
|
+
gem.homepage = "http://github.com/riscfuture/has_metadata"
|
17
|
+
gem.authors = [ "Tim Morgan" ]
|
18
|
+
gem.required_ruby_version = '>= 1.9'
|
19
|
+
gem.add_dependency "rails", ">= 3.0"
|
20
|
+
end
|
21
|
+
Jeweler::GemcutterTasks.new
|
22
|
+
|
23
|
+
require 'rspec/core/rake_task'
|
24
|
+
RSpec::Core::RakeTask.new
|
25
|
+
|
26
|
+
YARD::Rake::YardocTask.new('doc') do |doc|
|
27
|
+
doc.options << "-m" << "textile"
|
28
|
+
doc.options << "--protected"
|
29
|
+
doc.options << "-r" << "README.textile"
|
30
|
+
doc.options << "-o" << "doc"
|
31
|
+
doc.options << "--title" << "has_metadata Documentation".inspect
|
32
|
+
|
33
|
+
doc.files = [ 'lib/**/*', 'README.textile', 'templates/metadata.rb' ]
|
34
|
+
end
|
35
|
+
|
36
|
+
task(default: :spec)
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{has_metadata}
|
8
|
+
s.version = "1.0.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Tim Morgan"]
|
12
|
+
s.date = %q{2010-10-30}
|
13
|
+
s.description = %q{has_metadata lets you move non-indexed and weighty columns off of your big tables by creating a separate metadata table to store all this extra information. Works with Ruby 1.9. and Rails 3.0.}
|
14
|
+
s.email = %q{git@timothymorgan.info}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.textile"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".gitignore",
|
22
|
+
".rspec",
|
23
|
+
"Gemfile",
|
24
|
+
"Gemfile.lock",
|
25
|
+
"LICENSE",
|
26
|
+
"README.textile",
|
27
|
+
"Rakefile",
|
28
|
+
"VERSION",
|
29
|
+
"has_metadata.gemspec",
|
30
|
+
"lib/has_metadata.rb",
|
31
|
+
"lib/metadata_generator.rb",
|
32
|
+
"spec/has_metadata_spec.rb",
|
33
|
+
"spec/metadata_spec.rb",
|
34
|
+
"spec/spec_helper.rb",
|
35
|
+
"templates/create_metadata.rb",
|
36
|
+
"templates/metadata.rb"
|
37
|
+
]
|
38
|
+
s.homepage = %q{http://github.com/riscfuture/has_metadata}
|
39
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
40
|
+
s.require_paths = ["lib"]
|
41
|
+
s.required_ruby_version = Gem::Requirement.new(">= 1.9")
|
42
|
+
s.rubygems_version = %q{1.3.7}
|
43
|
+
s.summary = %q{Reduce your table width by moving non-indexed columns to a separate metadata table}
|
44
|
+
s.test_files = [
|
45
|
+
"spec/has_metadata_spec.rb",
|
46
|
+
"spec/metadata_spec.rb",
|
47
|
+
"spec/spec_helper.rb"
|
48
|
+
]
|
49
|
+
|
50
|
+
if s.respond_to? :specification_version then
|
51
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
52
|
+
s.specification_version = 3
|
53
|
+
|
54
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
55
|
+
s.add_runtime_dependency(%q<rails>, [">= 3.0"])
|
56
|
+
else
|
57
|
+
s.add_dependency(%q<rails>, [">= 3.0"])
|
58
|
+
end
|
59
|
+
else
|
60
|
+
s.add_dependency(%q<rails>, [">= 3.0"])
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
data/lib/has_metadata.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'metadata_generator'
|
2
|
+
|
3
|
+
# @private
|
4
|
+
class Object
|
5
|
+
|
6
|
+
# Creates a deep copy of this object.
|
7
|
+
#
|
8
|
+
# @raise [TypeError] If the object cannot be deep-copied. All objects that can
|
9
|
+
# be marshalled can be deep-copied.
|
10
|
+
|
11
|
+
def deep_clone
|
12
|
+
Marshal.load Marshal.dump(self)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
# Provides the {ClassMethods#has_metadata} method to subclasses of @ActiveRecord::Base@.
|
18
|
+
|
19
|
+
module HasMetadata
|
20
|
+
extend ActiveSupport::Concern
|
21
|
+
|
22
|
+
# Class methods that are added to your model.
|
23
|
+
|
24
|
+
module ClassMethods
|
25
|
+
|
26
|
+
# Defines a set of fields whose values exist in the associated {Metadata}
|
27
|
+
# record. Each key in the @fields@ hash is the name of a metadata field, and
|
28
|
+
# the value is a set of options to pass to the @validates@ method. If you do
|
29
|
+
# not want to perform any validation on a field, simply pass @true@ as its
|
30
|
+
# key value.
|
31
|
+
#
|
32
|
+
# In addition to the normal @validates@ keys, you can also include a @:type@
|
33
|
+
# key to restrict values to certain classes.
|
34
|
+
#
|
35
|
+
# @param [Hash<Symbol, Hash>] fields A mapping of field names to their validation options (and/or the @:type@ key).
|
36
|
+
#
|
37
|
+
# @example Three metadata fields, one basic, one validated, and one type-checked.
|
38
|
+
# has_metadata(optional: true, required: { presence: true }, number: { type: Fixnum })
|
39
|
+
|
40
|
+
def has_metadata(fields)
|
41
|
+
belongs_to :metadata, dependent: :destroy
|
42
|
+
accepts_nested_attributes_for :metadata
|
43
|
+
#after_save :save_metadata, if: :metadata_changed?
|
44
|
+
class_inheritable_hash :metadata_fields
|
45
|
+
self.metadata_fields = fields.deep_clone
|
46
|
+
|
47
|
+
#define_method(:save_metadata) { metadata.save! }
|
48
|
+
#define_method(:metadata_changed?) { metadata and metadata.changed? }
|
49
|
+
|
50
|
+
fields.each do |name, options|
|
51
|
+
delegate name, to: :metadata!
|
52
|
+
delegate :"#{name}=", to: :metadata!
|
53
|
+
|
54
|
+
if options.kind_of?(Hash) then
|
55
|
+
type = options.delete(:type)
|
56
|
+
validate do |obj|
|
57
|
+
errors.add(name, :incorrect_type) unless
|
58
|
+
metadata_typecast(obj.send(name), type).kind_of?(type) or
|
59
|
+
((options[:allow_nil] and obj.send(name).nil?) or (options[:allow_blank] and obj.send(name).blank?))
|
60
|
+
end if type
|
61
|
+
validates(name, options) unless options.empty? or (options.keys - [ :allow_nil, :allow_blank ]).empty?
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Instance methods that are added to your model.
|
68
|
+
|
69
|
+
module InstanceMethods
|
70
|
+
|
71
|
+
# @private
|
72
|
+
def metadata_typecast(value, type)
|
73
|
+
if value.kind_of?(String) then
|
74
|
+
if type == Integer or type == Fixnum then return value.to_i
|
75
|
+
elsif type == Float then return value.to_f end
|
76
|
+
end
|
77
|
+
return value
|
78
|
+
end
|
79
|
+
|
80
|
+
# @private
|
81
|
+
def assign_multiparameter_attributes(pairs)
|
82
|
+
fake_attributes = pairs.select { |(field, _)| self.class.metadata_fields.include? field[0, field.index('(')].to_sym }
|
83
|
+
|
84
|
+
fake_attributes.group_by { |(field, _)| field[0, field.index('(')] }.each do |field_name, parts|
|
85
|
+
options = self.class.metadata_fields[field_name.to_sym]
|
86
|
+
if options[:type] then
|
87
|
+
args = parts.each_with_object([]) do |(part_name, value), ary|
|
88
|
+
part_ann = part_name[part_name.index('(') + 1, part_name.length]
|
89
|
+
index = part_ann.to_i - 1
|
90
|
+
raise "Out-of-bounds multiparameter argument index" unless index >= 0
|
91
|
+
ary[index] = if value.blank? then nil
|
92
|
+
elsif part_ann.ends_with?('i)') then value.to_i
|
93
|
+
elsif part_ann.ends_with?('f)') then value.to_f
|
94
|
+
else value end
|
95
|
+
end
|
96
|
+
args.compact!
|
97
|
+
send :"#{field_name}=", options[:type].new(*args) unless args.empty?
|
98
|
+
else
|
99
|
+
raise "#{field_name} has no type and cannot be used for multiparameter assignment"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
super(pairs - fake_attributes)
|
104
|
+
end
|
105
|
+
|
106
|
+
# @return [Metadata] An existing associated {Metadata} instance, or new, saved one if none was found.
|
107
|
+
|
108
|
+
def metadata!
|
109
|
+
if instance_variables.include?(:@metadata) then
|
110
|
+
metadata.set_fields self.class.metadata_fields
|
111
|
+
else
|
112
|
+
Metadata.transaction do
|
113
|
+
(metadata || create_metadata).set_fields self.class.metadata_fields
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators'
|
3
|
+
require 'rails/generators/migration'
|
4
|
+
|
5
|
+
# @private
|
6
|
+
class MetadataGenerator < Rails::Generators::Base
|
7
|
+
include Rails::Generators::Migration
|
8
|
+
|
9
|
+
source_root "#{File.dirname __FILE__}/../templates"
|
10
|
+
|
11
|
+
def self.next_migration_number(dirname)
|
12
|
+
if ActiveRecord::Base.timestamped_migrations then
|
13
|
+
Time.now.utc.strftime "%Y%m%d%H%M%S"
|
14
|
+
else
|
15
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def copy_files
|
20
|
+
copy_file "metadata.rb", "app/models/metadata.rb"
|
21
|
+
migration_template "create_metadata.rb", "db/migrate/create_metadata.rb"
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
module SpecSupport
|
4
|
+
class ConstructorTester
|
5
|
+
attr_reader :args
|
6
|
+
def initialize(*args) @args = args end
|
7
|
+
end
|
8
|
+
|
9
|
+
class HasMetadataTester < ActiveRecord::Base
|
10
|
+
include HasMetadata
|
11
|
+
set_table_name 'users'
|
12
|
+
has_metadata({
|
13
|
+
untyped: {},
|
14
|
+
can_be_nil: { type: Date, allow_nil: true },
|
15
|
+
can_be_blank: { type: Date, allow_blank: true },
|
16
|
+
number: { numericality: true },
|
17
|
+
multiparam: { type: SpecSupport::ConstructorTester }
|
18
|
+
})
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe HasMetadata do
|
23
|
+
describe "#has_metadata" do
|
24
|
+
it "should add a :metadata association" do
|
25
|
+
SpecSupport::HasMetadataTester.reflect_on_association(:metadata).macro.should eql(:belongs_to)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should set the model to accept nested attributes for :metadata" do
|
29
|
+
SpecSupport::HasMetadataTester.nested_attributes_options[:metadata].should_not be_nil
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should make a getter for each field" do
|
33
|
+
SpecSupport::HasMetadataTester.new.should respond_to(:untyped)
|
34
|
+
SpecSupport::HasMetadataTester.new.should respond_to(:multiparam)
|
35
|
+
SpecSupport::HasMetadataTester.new.should respond_to(:number)
|
36
|
+
end
|
37
|
+
|
38
|
+
context "getters" do
|
39
|
+
before :each do
|
40
|
+
@object = SpecSupport::HasMetadataTester.new
|
41
|
+
@metadata = @object.metadata!
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should return a field in the metadata object" do
|
45
|
+
@metadata.data[:untyped] = 'bar'
|
46
|
+
@object.untyped.should eql('bar')
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should return nil if there is no associated metadata" do
|
50
|
+
@object.stub!(:metadata).and_return(nil)
|
51
|
+
ivars = @object.instance_variables - [ :@metadata ]
|
52
|
+
@object.stub!(:instance_variables).and_return(ivars)
|
53
|
+
|
54
|
+
@object.untyped.should be_nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should make a setter for each field" do
|
59
|
+
SpecSupport::HasMetadataTester.new.should respond_to(:untyped=)
|
60
|
+
SpecSupport::HasMetadataTester.new.should respond_to(:multiparam=)
|
61
|
+
SpecSupport::HasMetadataTester.new.should respond_to(:number=)
|
62
|
+
end
|
63
|
+
|
64
|
+
context "setters" do
|
65
|
+
before :each do
|
66
|
+
@object = SpecSupport::HasMetadataTester.new
|
67
|
+
@metadata = @object.metadata!
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should set the value in the metadata object" do
|
71
|
+
@object.untyped = 'foo'
|
72
|
+
@metadata.data[:untyped].should eql('foo')
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should create the metadata object if it doesn't exist" do
|
76
|
+
@object.stub!(:metadata).and_return(nil)
|
77
|
+
ivars = @object.instance_variables - [ :@metadata ]
|
78
|
+
@object.stub!(:instance_variables).and_return(ivars)
|
79
|
+
Metadata.should_receive(:new).once.and_return(@metadata)
|
80
|
+
|
81
|
+
@object.untyped = 'foo'
|
82
|
+
@metadata.data[:untyped].should eql('foo')
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should enforce a type if given" do
|
86
|
+
@object.multiparam = 'not correct'
|
87
|
+
@object.should_not be_valid
|
88
|
+
@object.errors[:multiparam].should_not be_empty
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should not enforce a type if :allow_nil is given" do
|
92
|
+
@object.can_be_nil = nil
|
93
|
+
@object.valid? #@object.should be_valid
|
94
|
+
@object.errors[:can_be_nil].should be_empty
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should not enforce a type if :allow_blank is given" do
|
98
|
+
@object.can_be_blank = ""
|
99
|
+
@object.valid? #@object.should be_valid
|
100
|
+
@object.errors[:can_be_blank].should be_empty
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should enforce other validations as given" do
|
104
|
+
@object.number = 'not number'
|
105
|
+
@object.should_not be_valid
|
106
|
+
@object.errors[:number].should_not be_empty
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should mass-assign a multiparameter attribute" do
|
110
|
+
@object.attributes = { 'multiparam(1)' => 'foo', 'multiparam(2)' => '1' }
|
111
|
+
@object.multiparam.args.should eql([ 'foo', '1' ])
|
112
|
+
end
|
113
|
+
|
114
|
+
it "should compact blank multiparameter parts" do
|
115
|
+
@object.attributes = { 'multiparam(1)' => '', 'multiparam(2)' => 'foo' }
|
116
|
+
@object.multiparam.args.should eql([ 'foo' ])
|
117
|
+
end
|
118
|
+
|
119
|
+
it "should typecast multiparameter parts" do
|
120
|
+
@object.attributes = { 'multiparam(1i)' => '1982', 'multiparam(2f)' => '10.5' }
|
121
|
+
@object.multiparam.args.should eql([ 1982, 10.5 ])
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe Metadata do
|
4
|
+
describe ".new" do
|
5
|
+
it "should initialize data to an empty hash" do
|
6
|
+
Metadata.new.data.should eql({})
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should initialize data to the value given in the initializer" do
|
10
|
+
Metadata.new(data: { foo: 'bar' }).data.should eql(foo: 'bar')
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
Bundler.require :default, :test
|
2
|
+
require 'active_support'
|
3
|
+
require 'active_record'
|
4
|
+
|
5
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
6
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
7
|
+
|
8
|
+
require 'has_metadata'
|
9
|
+
|
10
|
+
ActiveRecord::Base.establish_connection(
|
11
|
+
adapter: 'sqlite3',
|
12
|
+
database: 'test.sqlite'
|
13
|
+
)
|
14
|
+
require "#{File.dirname __FILE__}/../templates/metadata"
|
15
|
+
|
16
|
+
RSpec.configure do |config|
|
17
|
+
config.before(:each) do
|
18
|
+
Metadata.connection.execute "DROP TABLE IF EXISTS metadata"
|
19
|
+
Metadata.connection.execute "CREATE TABLE metadata (id INTEGER PRIMARY KEY ASC, data TEXT)"
|
20
|
+
Metadata.connection.execute "DROP TABLE IF EXISTS users"
|
21
|
+
Metadata.connection.execute "CREATE TABLE users (id INTEGER PRIMARY KEY ASC)"
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# Stores information about a model that doesn't need to be in that model's
|
2
|
+
# table. Each row in the @metadata@ table stores a schemaless, serialized hash
|
3
|
+
# of data associated with a model instance. Any model can have an associated row
|
4
|
+
# in the @metadata@ table by using the {HasMetadata} module.
|
5
|
+
#
|
6
|
+
# h2. Properties
|
7
|
+
#
|
8
|
+
# | @data@ | A hash of this metadata's contents (YAML serialized in the database). |
|
9
|
+
|
10
|
+
class Metadata < ActiveRecord::Base
|
11
|
+
set_table_name 'metadata'
|
12
|
+
serialize :data, Hash
|
13
|
+
|
14
|
+
after_initialize :initialize_data
|
15
|
+
before_save :nullify_empty_fields
|
16
|
+
|
17
|
+
validates :data,
|
18
|
+
presence: true
|
19
|
+
|
20
|
+
# @private
|
21
|
+
def set_fields(fields)
|
22
|
+
return self if @fields_set
|
23
|
+
@fields_set = true
|
24
|
+
|
25
|
+
fields.each do |name, _|
|
26
|
+
singleton_class.send(:define_method, name) { data[name] }
|
27
|
+
singleton_class.send(:define_method, :"#{name}=") { |value| data[name] = value }
|
28
|
+
end
|
29
|
+
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def initialize_data
|
36
|
+
self.data ||= Hash.new
|
37
|
+
end
|
38
|
+
|
39
|
+
def nullify_empty_fields
|
40
|
+
data.each { |key, value| data[key] = nil if data[key].blank? }
|
41
|
+
end
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: has_metadata
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 1
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
version: 1.0.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Tim Morgan
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-10-30 00:00:00 -07:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rails
|
22
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
23
|
+
none: false
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 3
|
29
|
+
- 0
|
30
|
+
version: "3.0"
|
31
|
+
type: :runtime
|
32
|
+
prerelease: false
|
33
|
+
version_requirements: *id001
|
34
|
+
description: has_metadata lets you move non-indexed and weighty columns off of your big tables by creating a separate metadata table to store all this extra information. Works with Ruby 1.9. and Rails 3.0.
|
35
|
+
email: git@timothymorgan.info
|
36
|
+
executables: []
|
37
|
+
|
38
|
+
extensions: []
|
39
|
+
|
40
|
+
extra_rdoc_files:
|
41
|
+
- LICENSE
|
42
|
+
- README.textile
|
43
|
+
files:
|
44
|
+
- .document
|
45
|
+
- .gitignore
|
46
|
+
- .rspec
|
47
|
+
- Gemfile
|
48
|
+
- Gemfile.lock
|
49
|
+
- LICENSE
|
50
|
+
- README.textile
|
51
|
+
- Rakefile
|
52
|
+
- VERSION
|
53
|
+
- has_metadata.gemspec
|
54
|
+
- lib/has_metadata.rb
|
55
|
+
- lib/metadata_generator.rb
|
56
|
+
- spec/has_metadata_spec.rb
|
57
|
+
- spec/metadata_spec.rb
|
58
|
+
- spec/spec_helper.rb
|
59
|
+
- templates/create_metadata.rb
|
60
|
+
- templates/metadata.rb
|
61
|
+
has_rdoc: true
|
62
|
+
homepage: http://github.com/riscfuture/has_metadata
|
63
|
+
licenses: []
|
64
|
+
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options:
|
67
|
+
- --charset=UTF-8
|
68
|
+
require_paths:
|
69
|
+
- lib
|
70
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
segments:
|
76
|
+
- 1
|
77
|
+
- 9
|
78
|
+
version: "1.9"
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
+
none: false
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
segments:
|
85
|
+
- 0
|
86
|
+
version: "0"
|
87
|
+
requirements: []
|
88
|
+
|
89
|
+
rubyforge_project:
|
90
|
+
rubygems_version: 1.3.7
|
91
|
+
signing_key:
|
92
|
+
specification_version: 3
|
93
|
+
summary: Reduce your table width by moving non-indexed columns to a separate metadata table
|
94
|
+
test_files:
|
95
|
+
- spec/has_metadata_spec.rb
|
96
|
+
- spec/metadata_spec.rb
|
97
|
+
- spec/spec_helper.rb
|