has_metadata 1.0.0
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/.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
|