has_metadata 1.6.0 → 1.6.2
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.
- checksums.yaml +7 -0
- data/README.md +117 -0
- data/has_metadata.gemspec +17 -7
- data/lib/has_metadata/metadata_generator.rb +6 -6
- data/lib/has_metadata/model.rb +16 -7
- data/lib/has_metadata.rb +70 -35
- data/templates/metadata.rb +7 -4
- metadata +69 -45
- data/README.textile +0 -126
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e2f29c8739a41a97beed0ed3c6b331f8ba3cc2da4ee8bacbd7dc269a0113d23a
|
|
4
|
+
data.tar.gz: 9ceb1f806883a0a42a8734270801b88f715357fb8315f8dbf51566d84a4caaad
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 91d5b70471591f8c37c8ae7d6849e370356b6f0fb42c324b7e5a451c0b3448564060f178be7c1ec853f53d4302f8eb2acd7ad1f0d13270fd69bd4ac0a19dfc57
|
|
7
|
+
data.tar.gz: a53e2493826bd95af4c129a6fa3b4a73acfee451f1b70797d48fa4803242662d54bda4324b7627204a90718fbc2db65edec46efb60f8fa13d7d38a1ef1ee4736
|
data/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
> ⚠️ **DEPRECATED:** `has_metadata` is no longer maintained. Superseded by
|
|
2
|
+
> [`has_metadata_column`](https://github.com/RISCfuture/has_metadata_column)
|
|
3
|
+
> (also deprecated). No active maintenance. The final release is v1.6.2.
|
|
4
|
+
|
|
5
|
+
has_metadata
|
|
6
|
+
============
|
|
7
|
+
|
|
8
|
+
**Keep your tables narrow**
|
|
9
|
+
|
|
10
|
+
| | |
|
|
11
|
+
|:------------|:--------------------------------|
|
|
12
|
+
| **Author** | Tim Morgan |
|
|
13
|
+
| **Version** | 1.6.1 (Jan 24, 2012) |
|
|
14
|
+
| **License** | Released under the MIT License. |
|
|
15
|
+
|
|
16
|
+
About
|
|
17
|
+
-----
|
|
18
|
+
|
|
19
|
+
Wide tables are a problem for big databases. If your `ActiveRecord` models have
|
|
20
|
+
10, maybe 15 columns, some of which are `VARCHARs` or maybe even `TEXTs`, it's
|
|
21
|
+
going to slow your queries down when you start to scale up.
|
|
22
|
+
|
|
23
|
+
The easy solution to this problem is to limit your projections; in other words,
|
|
24
|
+
to only `SELECT` the columns that you actually need. If you've got a `users`
|
|
25
|
+
table with a giant `about_me` text column, and you're only trying to look up the
|
|
26
|
+
user's login, then just select the `login` column.
|
|
27
|
+
|
|
28
|
+
In the long run, though, a superior solution is to just move those
|
|
29
|
+
`about_me`-type columns to a completely different table. This table has just one
|
|
30
|
+
JSON-serialized field, making it schemaless, so it doesn't waste space. Each row
|
|
31
|
+
in this table is associated with a record in another table (`Metadata` `has_one`
|
|
32
|
+
of your models).
|
|
33
|
+
|
|
34
|
+
This way, when your website gets huge, all of your giant, freeform data is in
|
|
35
|
+
one table that you can shard, or move off to an alternate database, or even a
|
|
36
|
+
NoSQL-type document store, or otherwise manage as you please. Your relational
|
|
37
|
+
tables remain slim and efficient, containing only columns that a) are indexed,
|
|
38
|
+
or b) you need frequent access to.
|
|
39
|
+
|
|
40
|
+
This gem includes a generator that creates the `Metadata` model, and a module
|
|
41
|
+
that you can include in your models to define which fields have been spun off to
|
|
42
|
+
the metadata record.
|
|
43
|
+
|
|
44
|
+
Installation
|
|
45
|
+
------------
|
|
46
|
+
|
|
47
|
+
**Important Note:** This gem is only compatible with Ruby 1.9+ and Rails 3.0+.
|
|
48
|
+
|
|
49
|
+
Firstly, add the gem to your Rails project's `Gemfile`:
|
|
50
|
+
|
|
51
|
+
```` ruby
|
|
52
|
+
gem 'has_metadata'
|
|
53
|
+
````
|
|
54
|
+
|
|
55
|
+
Next, run the generator, which will add the `Metadata` model and its migration
|
|
56
|
+
to your application.
|
|
57
|
+
|
|
58
|
+
```` sh
|
|
59
|
+
rails generate metadata
|
|
60
|
+
````
|
|
61
|
+
|
|
62
|
+
Usage
|
|
63
|
+
-----
|
|
64
|
+
|
|
65
|
+
The first thing to think about is what columns to keep in your model. You will
|
|
66
|
+
need to keep any indexed columns, or any columns you perform lookups or other
|
|
67
|
+
SQL queries with. You should also keep any frequently accessed columns,
|
|
68
|
+
especially if they are small (integers or booleans). Good candidates for the
|
|
69
|
+
metadata table are the `TEXT`- and `VARCHAR`-type columns that you only need to
|
|
70
|
+
render a page or two in your app.
|
|
71
|
+
|
|
72
|
+
You'll need to change your model's schema so that it has a `metadata_id` column
|
|
73
|
+
that will associate the model with its `Metadata` instance:
|
|
74
|
+
|
|
75
|
+
```` ruby
|
|
76
|
+
t.belongs_to :metadata
|
|
77
|
+
````
|
|
78
|
+
|
|
79
|
+
Next, include the `HasMetadata` module in your model, and call the
|
|
80
|
+
`has_metadata` method to define the schema of your metadata. You can get more
|
|
81
|
+
information in the {HasMetadata::ClassMethods#has_metadata} documentation, but for starters, here's a
|
|
82
|
+
basic example:
|
|
83
|
+
|
|
84
|
+
```` ruby
|
|
85
|
+
class User < ActiveRecord::Base
|
|
86
|
+
include HasMetadata
|
|
87
|
+
has_metadata({
|
|
88
|
+
about_me: { type: String, length: { maximum: 512 } },
|
|
89
|
+
birthdate: { type: Date, presence: true },
|
|
90
|
+
zipcode: { type: Number, numericality: { greater_than: 9999, less_than: 10_000} }
|
|
91
|
+
})
|
|
92
|
+
end
|
|
93
|
+
````
|
|
94
|
+
|
|
95
|
+
As you can see, you pass field names mapped to a hash. The hash describes the
|
|
96
|
+
validation that will be performed, and is in the same format as a call to
|
|
97
|
+
`validates`. In addition to the `EachValidator` keys shown above, you can also
|
|
98
|
+
pass a `type` key, to constrain the Ruby type that can be assigned to the field.
|
|
99
|
+
|
|
100
|
+
Each of these fields (in this case, `about_me`, `birthdate`, and `zipcode`) can
|
|
101
|
+
be accessed and set as first_level methods on an instance of your model:
|
|
102
|
+
|
|
103
|
+
```` ruby
|
|
104
|
+
user.about_me #=> "I was born in 1982 in Aberdeen. My father was a carpenter from..."
|
|
105
|
+
````
|
|
106
|
+
|
|
107
|
+
... and thus, used as part of `form_for` fields:
|
|
108
|
+
|
|
109
|
+
```` ruby
|
|
110
|
+
form_for user do |f|
|
|
111
|
+
f.text_area :about_me, rows: 5, cols: 80
|
|
112
|
+
end
|
|
113
|
+
````
|
|
114
|
+
|
|
115
|
+
The only thing you _can't_ do is use these fields in a query, obviously. You
|
|
116
|
+
can't do something like `User.where(zipcode: 90210)`, because that column
|
|
117
|
+
doesn't exist on the `users` table.
|
data/has_metadata.gemspec
CHANGED
|
@@ -2,23 +2,24 @@
|
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
|
3
3
|
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
|
4
4
|
# -*- encoding: utf-8 -*-
|
|
5
|
+
# stub: has_metadata 1.6.2 ruby lib
|
|
5
6
|
|
|
6
7
|
Gem::Specification.new do |s|
|
|
7
8
|
s.name = "has_metadata"
|
|
8
|
-
s.version = "1.6.
|
|
9
|
+
s.version = "1.6.2"
|
|
9
10
|
|
|
10
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
|
11
12
|
s.authors = ["Tim Morgan"]
|
|
12
|
-
s.date = "
|
|
13
|
+
s.date = "2013-12-08"
|
|
13
14
|
s.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."
|
|
14
15
|
s.email = "git@timothymorgan.info"
|
|
15
16
|
s.extra_rdoc_files = [
|
|
16
17
|
"LICENSE",
|
|
17
|
-
"README.
|
|
18
|
+
"README.md"
|
|
18
19
|
]
|
|
19
20
|
s.files = [
|
|
20
21
|
"LICENSE",
|
|
21
|
-
"README.
|
|
22
|
+
"README.md",
|
|
22
23
|
"has_metadata.gemspec",
|
|
23
24
|
"lib/has_metadata.rb",
|
|
24
25
|
"lib/has_metadata/metadata_generator.rb",
|
|
@@ -29,11 +30,20 @@ Gem::Specification.new do |s|
|
|
|
29
30
|
s.homepage = "http://github.com/riscfuture/has_metadata"
|
|
30
31
|
s.require_paths = ["lib"]
|
|
31
32
|
s.required_ruby_version = Gem::Requirement.new(">= 1.9")
|
|
32
|
-
s.rubygems_version = "1.
|
|
33
|
-
s.summary = "Reduce your table width by moving non-indexed columns to a separate metadata table"
|
|
33
|
+
s.rubygems_version = "2.1.11"
|
|
34
|
+
s.summary = "[DEPRECATED] Reduce your table width by moving non-indexed columns to a separate metadata table"
|
|
35
|
+
s.post_install_message = <<~MSG
|
|
36
|
+
|
|
37
|
+
⚠️ DEPRECATED: has_metadata is no longer maintained.
|
|
38
|
+
|
|
39
|
+
Superseded by `has_metadata_column` (also deprecated as of this notice). No active maintenance.
|
|
40
|
+
|
|
41
|
+
This is the final release. No further updates are planned.
|
|
42
|
+
|
|
43
|
+
MSG
|
|
34
44
|
|
|
35
45
|
if s.respond_to? :specification_version then
|
|
36
|
-
s.specification_version =
|
|
46
|
+
s.specification_version = 4
|
|
37
47
|
|
|
38
48
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
|
39
49
|
s.add_runtime_dependency(%q<rails>, [">= 3.2"])
|
|
@@ -5,19 +5,19 @@ require 'rails/generators/migration'
|
|
|
5
5
|
# @private
|
|
6
6
|
class MetadataGenerator < Rails::Generators::Base
|
|
7
7
|
include Rails::Generators::Migration
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
source_root "#{File.dirname __FILE__}/../../templates"
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
def self.next_migration_number(dirname)
|
|
12
12
|
if ActiveRecord::Base.timestamped_migrations then
|
|
13
|
-
Time.now.utc.strftime
|
|
13
|
+
Time.now.utc.strftime '%Y%m%d%H%M%S'
|
|
14
14
|
else
|
|
15
15
|
"%.3d" % (current_migration_number(dirname) + 1)
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
def copy_files
|
|
20
|
-
copy_file
|
|
21
|
-
migration_template
|
|
20
|
+
copy_file 'metadata.rb', 'app/models/metadata.rb'
|
|
21
|
+
migration_template 'create_metadata.rb', 'db/migrate/create_metadata.rb'
|
|
22
22
|
end
|
|
23
23
|
end
|
data/lib/has_metadata/model.rb
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
require 'active_record'
|
|
2
|
+
|
|
1
3
|
module HasMetadata
|
|
2
|
-
|
|
4
|
+
|
|
3
5
|
# Base class of the {Metadata} model. Functionality is moved to this class to
|
|
4
|
-
# make changes to the model easier. See the
|
|
6
|
+
# make changes to the model easier. See the `Metadata` method for more
|
|
5
7
|
# information.
|
|
6
|
-
|
|
8
|
+
|
|
7
9
|
class Model < ActiveRecord::Base
|
|
8
10
|
self.table_name = 'metadata'
|
|
9
11
|
serialize :data, Hash
|
|
@@ -20,10 +22,10 @@ module HasMetadata
|
|
|
20
22
|
name = name.to_sym
|
|
21
23
|
super(name) or fields.include?(name)
|
|
22
24
|
end
|
|
23
|
-
|
|
25
|
+
|
|
24
26
|
# override attribute prefix/suffix methods to get full first-class
|
|
25
27
|
# property functionality
|
|
26
|
-
|
|
28
|
+
|
|
27
29
|
singleton_class.send(:define_method, :attribute) do |name|
|
|
28
30
|
name = name.to_sym
|
|
29
31
|
super(name) unless fields.include?(name)
|
|
@@ -33,7 +35,7 @@ module HasMetadata
|
|
|
33
35
|
data.include?(name) ? data[name] : default
|
|
34
36
|
end
|
|
35
37
|
singleton_class.send :alias_method, :attribute_before_type_cast, :attribute
|
|
36
|
-
|
|
38
|
+
|
|
37
39
|
singleton_class.send(:define_method, :query_attribute) do |name|
|
|
38
40
|
name = name.to_sym
|
|
39
41
|
super(name) unless fields.include?(name)
|
|
@@ -49,19 +51,26 @@ module HasMetadata
|
|
|
49
51
|
not send(name).blank?
|
|
50
52
|
end
|
|
51
53
|
end
|
|
52
|
-
|
|
54
|
+
|
|
53
55
|
singleton_class.send(:define_method, :attribute=) do |name, value|
|
|
54
56
|
name = name.to_sym
|
|
55
57
|
super(name, value) unless fields.include?(name)
|
|
56
58
|
|
|
57
59
|
options = fields[name] || {}
|
|
58
60
|
data_will_change!
|
|
61
|
+
attribute_will_change! name.to_s
|
|
59
62
|
data[name] = HasMetadata.metadata_typecast(value, options[:type])
|
|
60
63
|
end
|
|
61
64
|
|
|
62
65
|
self
|
|
63
66
|
end
|
|
64
67
|
|
|
68
|
+
# @return [Hash<String, Object>] A hash of metadata fields that have been
|
|
69
|
+
# altered.
|
|
70
|
+
def changed_metadata
|
|
71
|
+
changed_attributes.except(*attribute_names)
|
|
72
|
+
end
|
|
73
|
+
|
|
65
74
|
private
|
|
66
75
|
|
|
67
76
|
def initialize_data
|
data/lib/has_metadata.rb
CHANGED
|
@@ -16,15 +16,16 @@ class Object
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
# Provides the {ClassMethods#has_metadata} method to subclasses of
|
|
19
|
+
# Provides the {ClassMethods#has_metadata} method to subclasses of
|
|
20
|
+
# `ActiveRecord::Base`.
|
|
20
21
|
|
|
21
22
|
module HasMetadata
|
|
22
23
|
extend ActiveSupport::Concern
|
|
23
|
-
|
|
24
|
+
|
|
24
25
|
# @private
|
|
25
26
|
def self.metadata_typecast(value, type)
|
|
26
27
|
if value.kind_of?(String) then
|
|
27
|
-
if type == Integer or type ==
|
|
28
|
+
if type == Integer or type == Integer then
|
|
28
29
|
begin
|
|
29
30
|
return Integer(value.sub(/^0+/, '')) # so that it doesn't think it's in octal
|
|
30
31
|
rescue ArgumentError
|
|
@@ -36,33 +37,37 @@ module HasMetadata
|
|
|
36
37
|
rescue ArgumentError
|
|
37
38
|
return value
|
|
38
39
|
end
|
|
39
|
-
elsif type == Boolean then
|
|
40
|
+
elsif type == Boolean then
|
|
41
|
+
return value.parse_bool
|
|
42
|
+
end
|
|
40
43
|
end
|
|
41
44
|
return value
|
|
42
45
|
end
|
|
43
|
-
|
|
46
|
+
|
|
44
47
|
# Class methods that are added to your model.
|
|
45
|
-
|
|
48
|
+
|
|
46
49
|
module ClassMethods
|
|
47
50
|
|
|
48
51
|
# Defines a set of fields whose values exist in the associated {Metadata}
|
|
49
|
-
# record. Each key in the
|
|
50
|
-
# the value is a set of options to pass to the
|
|
51
|
-
# not want to perform any validation on a field, simply pass
|
|
52
|
+
# record. Each key in the `fields` hash is the name of a metadata field, and
|
|
53
|
+
# the value is a set of options to pass to the `validates` method. If you do
|
|
54
|
+
# not want to perform any validation on a field, simply pass `true` as its
|
|
52
55
|
# key value.
|
|
53
56
|
#
|
|
54
|
-
# In addition to the normal
|
|
55
|
-
# key to restrict values to certain classes, or a
|
|
57
|
+
# In addition to the normal `validates` keys, you can also include a `:type`
|
|
58
|
+
# key to restrict values to certain classes, or a `:default` key to specify
|
|
56
59
|
# a value to return for the getter should none be set (normal default is
|
|
57
|
-
#
|
|
60
|
+
# `nil`).
|
|
58
61
|
#
|
|
59
62
|
# @param [Hash<Symbol, Hash>] fields A mapping of field names to their
|
|
60
|
-
# validation options (and/or the
|
|
63
|
+
# validation options (and/or the `:type` key).
|
|
61
64
|
#
|
|
62
65
|
# @example Three metadata fields, one basic, one validated, and one type-checked.
|
|
63
|
-
# has_metadata(optional: true, required: { presence: true }, number: { type:
|
|
66
|
+
# has_metadata(optional: true, required: { presence: true }, number: { type: Integer })
|
|
64
67
|
|
|
65
68
|
def has_metadata(fields)
|
|
69
|
+
raise "Can't define Rails-magic timestamped columns as metadata" if (fields.keys & [:created_at, :created_on, :updated_at, :updated_on]).any?
|
|
70
|
+
|
|
66
71
|
if !respond_to?(:metadata_fields) then
|
|
67
72
|
belongs_to :metadata, dependent: :destroy
|
|
68
73
|
accepts_nested_attributes_for :metadata
|
|
@@ -73,7 +78,9 @@ module HasMetadata
|
|
|
73
78
|
|
|
74
79
|
define_method(:save_metadata) { metadata.save! }
|
|
75
80
|
define_method(:metadata_changed?) { metadata.try :changed? }
|
|
76
|
-
|
|
81
|
+
|
|
82
|
+
prepend WithMetadata
|
|
83
|
+
elsif metadata_fields.slice(*fields.keys) != fields
|
|
77
84
|
raise "Cannot redefine existing metadata fields: #{(fields.keys & self.metadata_fields.keys).to_sentence}" unless (fields.keys & self.metadata_fields.keys).empty?
|
|
78
85
|
self.metadata_fields = self.metadata_fields.merge(fields)
|
|
79
86
|
end
|
|
@@ -81,33 +88,34 @@ module HasMetadata
|
|
|
81
88
|
fields.each do |name, options|
|
|
82
89
|
# delegate all attribute methods to the metadata
|
|
83
90
|
attribute_method_matchers.each { |matcher| delegate matcher.method_name(name), to: :metadata! }
|
|
84
|
-
|
|
91
|
+
|
|
85
92
|
if options.kind_of?(Hash) then
|
|
86
|
-
type
|
|
93
|
+
type = options.delete(:type)
|
|
87
94
|
type_validate = !options.delete(:skip_type_validation)
|
|
88
95
|
options.delete :default
|
|
89
|
-
|
|
96
|
+
|
|
90
97
|
validate do |obj|
|
|
91
98
|
value = obj.send(name)
|
|
92
|
-
errors.add(name, :incorrect_type) unless
|
|
93
|
-
HasMetadata.metadata_typecast(value, type).kind_of?(type) or
|
|
99
|
+
errors.add(name, :incorrect_type) unless HasMetadata.metadata_typecast(value, type).kind_of?(type) or
|
|
94
100
|
((options[:allow_nil] and value.nil?) or (options[:allow_blank] and value.blank?))
|
|
95
101
|
end if type && type_validate
|
|
96
|
-
validates(name, options) unless options.empty? or (options.keys - [
|
|
102
|
+
validates(name, options) unless options.empty? or (options.keys - [:allow_nil, :allow_blank]).empty?
|
|
97
103
|
end
|
|
98
104
|
end
|
|
99
105
|
end
|
|
100
106
|
end
|
|
101
107
|
|
|
108
|
+
# @private
|
|
102
109
|
def as_json(options={})
|
|
103
|
-
options
|
|
104
|
-
options[:except]
|
|
110
|
+
options ||= Hash.new # the JSON encoder can sometimes give us nil options?
|
|
111
|
+
options[:except] = Array.wrap(options[:except]) + [:metadata_id]
|
|
105
112
|
options[:methods] = Array.wrap(options[:methods]) + metadata_fields.keys - options[:except].map(&:to_sym)
|
|
106
113
|
super options
|
|
107
114
|
end
|
|
108
|
-
|
|
115
|
+
|
|
116
|
+
# @private
|
|
109
117
|
def to_xml(options={})
|
|
110
|
-
options[:except]
|
|
118
|
+
options[:except] = Array.wrap(options[:except]) + [:metadata_id]
|
|
111
119
|
options[:methods] = Array.wrap(options[:methods]) + metadata_fields.keys - options[:except].map(&:to_sym)
|
|
112
120
|
super options
|
|
113
121
|
end
|
|
@@ -115,18 +123,20 @@ module HasMetadata
|
|
|
115
123
|
# @private
|
|
116
124
|
def assign_multiparameter_attributes(pairs)
|
|
117
125
|
fake_attributes = pairs.select { |(field, _)| self.class.metadata_fields.include? field[0, field.index('(')].to_sym }
|
|
126
|
+
result = super(pairs.except(fake_attributes.keys))
|
|
118
127
|
|
|
119
128
|
fake_attributes.group_by { |(field, _)| field[0, field.index('(')] }.each do |field_name, parts|
|
|
120
129
|
options = self.class.metadata_fields[field_name.to_sym]
|
|
121
130
|
if options[:type] then
|
|
122
131
|
args = parts.each_with_object([]) do |(part_name, value), ary|
|
|
123
132
|
part_ann = part_name[part_name.index('(') + 1, part_name.length]
|
|
124
|
-
index
|
|
133
|
+
index = part_ann.to_i - 1
|
|
125
134
|
raise "Out-of-bounds multiparameter argument index" unless index >= 0
|
|
126
135
|
ary[index] = if value.blank? then nil
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
136
|
+
elsif part_ann.ends_with?('i)') then value.to_i
|
|
137
|
+
elsif part_ann.ends_with?('f)') then value.to_f
|
|
138
|
+
else value
|
|
139
|
+
end
|
|
130
140
|
end
|
|
131
141
|
args.compact!
|
|
132
142
|
send :"#{field_name}=", options[:type].new(*args) unless args.empty?
|
|
@@ -135,22 +145,47 @@ module HasMetadata
|
|
|
135
145
|
end
|
|
136
146
|
end
|
|
137
147
|
|
|
138
|
-
|
|
148
|
+
return result
|
|
139
149
|
end
|
|
140
150
|
|
|
141
151
|
# @return [Metadata] An existing associated {Metadata} instance, or new,
|
|
142
152
|
# saved one if none was found.
|
|
143
153
|
|
|
144
154
|
def metadata!
|
|
145
|
-
if instance_variables.include?(:@metadata) then
|
|
146
|
-
metadata
|
|
155
|
+
if instance_variables.include?(:@metadata) && metadata then
|
|
156
|
+
metadata
|
|
147
157
|
else
|
|
148
|
-
|
|
149
|
-
|
|
158
|
+
if new_record? then
|
|
159
|
+
metadata || build_metadata
|
|
160
|
+
else
|
|
161
|
+
metadata || Metadata.transaction { metadata || create_metadata }
|
|
162
|
+
end
|
|
163
|
+
end.set_fields self.class.metadata_fields
|
|
150
164
|
end
|
|
151
165
|
|
|
152
166
|
# @private
|
|
153
167
|
def inspect
|
|
154
|
-
"#<#{self.class.to_s} #{attributes.merge(metadata.try(:data).try(:stringify_keys) || {}).map { |k,v| "#{k}: #{v.inspect}" }.join(', ')}>"
|
|
168
|
+
"#<#{self.class.to_s} #{attributes.merge(metadata.try(:data).try(:stringify_keys) || {}).map { |k, v| "#{k}: #{v.inspect}" }.join(', ')}>"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
module WithMetadata
|
|
172
|
+
# @private
|
|
173
|
+
def changed_attributes
|
|
174
|
+
super.merge(metadata.try(:changed_metadata) || {})
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# @private
|
|
178
|
+
def attributes_changed_by_setter
|
|
179
|
+
super.merge(metadata.try(:changed_metadata) || {})
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# @private
|
|
183
|
+
def attribute_will_change!(attr)
|
|
184
|
+
if attribute_names.include?(attr) then
|
|
185
|
+
super attr
|
|
186
|
+
else
|
|
187
|
+
metadata!.send :attribute_will_change!, attr
|
|
188
|
+
end
|
|
189
|
+
end
|
|
155
190
|
end
|
|
156
191
|
end
|
data/templates/metadata.rb
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# Stores information about a model that doesn't need to be in that model's
|
|
2
|
-
# table. Each row in the
|
|
2
|
+
# table. Each row in the `metadata` table stores a schemaless, serialized hash
|
|
3
3
|
# of data associated with a model instance. Any model can have an associated row
|
|
4
|
-
# in the
|
|
4
|
+
# in the `metadata` table by using the {HasMetadata} module.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
6
|
+
# Properties
|
|
7
|
+
# ----------
|
|
7
8
|
#
|
|
8
|
-
# |
|
|
9
|
+
# | | |
|
|
10
|
+
# |:-------|:----------------------------------------------------------------------|
|
|
11
|
+
# | `data` | A hash of this metadata's contents (YAML serialized in the database). |
|
|
9
12
|
|
|
10
13
|
class Metadata < HasMetadata::Model
|
|
11
14
|
end
|
metadata
CHANGED
|
@@ -1,93 +1,112 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: has_metadata
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.6.
|
|
5
|
-
prerelease:
|
|
4
|
+
version: 1.6.2
|
|
6
5
|
platform: ruby
|
|
7
6
|
authors:
|
|
8
7
|
- Tim Morgan
|
|
9
|
-
autorequire:
|
|
10
8
|
bindir: bin
|
|
11
9
|
cert_chain: []
|
|
12
|
-
date:
|
|
10
|
+
date: 2013-12-08 00:00:00.000000000 Z
|
|
13
11
|
dependencies:
|
|
14
12
|
- !ruby/object:Gem::Dependency
|
|
15
13
|
name: rails
|
|
16
|
-
requirement:
|
|
17
|
-
none: false
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
18
15
|
requirements:
|
|
19
|
-
- -
|
|
16
|
+
- - ">="
|
|
20
17
|
- !ruby/object:Gem::Version
|
|
21
18
|
version: '3.2'
|
|
22
19
|
type: :runtime
|
|
23
20
|
prerelease: false
|
|
24
|
-
version_requirements:
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.2'
|
|
25
26
|
- !ruby/object:Gem::Dependency
|
|
26
27
|
name: boolean
|
|
27
|
-
requirement:
|
|
28
|
-
none: false
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
|
-
- -
|
|
30
|
+
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
32
|
version: '0'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
|
-
version_requirements:
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
36
40
|
- !ruby/object:Gem::Dependency
|
|
37
41
|
name: jeweler
|
|
38
|
-
requirement:
|
|
39
|
-
none: false
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
40
43
|
requirements:
|
|
41
|
-
- -
|
|
44
|
+
- - ">="
|
|
42
45
|
- !ruby/object:Gem::Version
|
|
43
46
|
version: '0'
|
|
44
47
|
type: :development
|
|
45
48
|
prerelease: false
|
|
46
|
-
version_requirements:
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
47
54
|
- !ruby/object:Gem::Dependency
|
|
48
55
|
name: yard
|
|
49
|
-
requirement:
|
|
50
|
-
none: false
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
51
57
|
requirements:
|
|
52
|
-
- -
|
|
58
|
+
- - ">="
|
|
53
59
|
- !ruby/object:Gem::Version
|
|
54
60
|
version: '0'
|
|
55
61
|
type: :development
|
|
56
62
|
prerelease: false
|
|
57
|
-
version_requirements:
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
58
68
|
- !ruby/object:Gem::Dependency
|
|
59
69
|
name: RedCloth
|
|
60
|
-
requirement:
|
|
61
|
-
none: false
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
62
71
|
requirements:
|
|
63
|
-
- -
|
|
72
|
+
- - ">="
|
|
64
73
|
- !ruby/object:Gem::Version
|
|
65
74
|
version: '0'
|
|
66
75
|
type: :development
|
|
67
76
|
prerelease: false
|
|
68
|
-
version_requirements:
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
69
82
|
- !ruby/object:Gem::Dependency
|
|
70
83
|
name: sqlite3
|
|
71
|
-
requirement:
|
|
72
|
-
none: false
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
73
85
|
requirements:
|
|
74
|
-
- -
|
|
86
|
+
- - ">="
|
|
75
87
|
- !ruby/object:Gem::Version
|
|
76
88
|
version: '0'
|
|
77
89
|
type: :development
|
|
78
90
|
prerelease: false
|
|
79
|
-
version_requirements:
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
80
96
|
- !ruby/object:Gem::Dependency
|
|
81
97
|
name: rspec
|
|
82
|
-
requirement:
|
|
83
|
-
none: false
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
84
99
|
requirements:
|
|
85
|
-
- -
|
|
100
|
+
- - ">="
|
|
86
101
|
- !ruby/object:Gem::Version
|
|
87
102
|
version: '0'
|
|
88
103
|
type: :development
|
|
89
104
|
prerelease: false
|
|
90
|
-
version_requirements:
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0'
|
|
91
110
|
description: has_metadata lets you move non-indexed and weighty columns off of your
|
|
92
111
|
big tables by creating a separate metadata table to store all this extra information.
|
|
93
112
|
Works with Ruby 1.9. and Rails 3.0.
|
|
@@ -96,10 +115,10 @@ executables: []
|
|
|
96
115
|
extensions: []
|
|
97
116
|
extra_rdoc_files:
|
|
98
117
|
- LICENSE
|
|
99
|
-
- README.
|
|
118
|
+
- README.md
|
|
100
119
|
files:
|
|
101
120
|
- LICENSE
|
|
102
|
-
- README.
|
|
121
|
+
- README.md
|
|
103
122
|
- has_metadata.gemspec
|
|
104
123
|
- lib/has_metadata.rb
|
|
105
124
|
- lib/has_metadata/metadata_generator.rb
|
|
@@ -108,27 +127,32 @@ files:
|
|
|
108
127
|
- templates/metadata.rb
|
|
109
128
|
homepage: http://github.com/riscfuture/has_metadata
|
|
110
129
|
licenses: []
|
|
111
|
-
|
|
130
|
+
metadata: {}
|
|
131
|
+
post_install_message: |2+
|
|
132
|
+
|
|
133
|
+
⚠️ DEPRECATED: has_metadata is no longer maintained.
|
|
134
|
+
|
|
135
|
+
Superseded by `has_metadata_column` (also deprecated as of this notice). No active maintenance.
|
|
136
|
+
|
|
137
|
+
This is the final release. No further updates are planned.
|
|
138
|
+
|
|
112
139
|
rdoc_options: []
|
|
113
140
|
require_paths:
|
|
114
141
|
- lib
|
|
115
142
|
required_ruby_version: !ruby/object:Gem::Requirement
|
|
116
|
-
none: false
|
|
117
143
|
requirements:
|
|
118
|
-
- -
|
|
144
|
+
- - ">="
|
|
119
145
|
- !ruby/object:Gem::Version
|
|
120
146
|
version: '1.9'
|
|
121
147
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
122
|
-
none: false
|
|
123
148
|
requirements:
|
|
124
|
-
- -
|
|
149
|
+
- - ">="
|
|
125
150
|
- !ruby/object:Gem::Version
|
|
126
151
|
version: '0'
|
|
127
152
|
requirements: []
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
summary: Reduce your table width by moving non-indexed columns to a separate metadata
|
|
133
|
-
table
|
|
153
|
+
rubygems_version: 4.0.11
|
|
154
|
+
specification_version: 4
|
|
155
|
+
summary: "[DEPRECATED] Reduce your table width by moving non-indexed columns to a
|
|
156
|
+
separate metadata table"
|
|
134
157
|
test_files: []
|
|
158
|
+
...
|
data/README.textile
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
h1. has_metadata -- Keep your tables narrow
|
|
2
|
-
|
|
3
|
-
| *Author* | Tim Morgan |
|
|
4
|
-
| *Version* | 1.6 (Jan 23, 2012) |
|
|
5
|
-
| *License* | Released under the MIT License. |
|
|
6
|
-
|
|
7
|
-
h2. Important note for those upgrading from 1.0 to 1.1
|
|
8
|
-
|
|
9
|
-
You must re-run the @rails generate metadata@ task to update your @Metadata@
|
|
10
|
-
model. When finished, your @app/models/metadata.rb@ file should look like:
|
|
11
|
-
|
|
12
|
-
<pre><code>
|
|
13
|
-
# Stores information about a model that doesn't need to be in that model's
|
|
14
|
-
# table. Each row in the @metadata@ table stores a schemaless, serialized hash
|
|
15
|
-
# of data associated with a model instance. Any model can have an associated row
|
|
16
|
-
# in the @metadata@ table by using the {HasMetadata} module.
|
|
17
|
-
#
|
|
18
|
-
# h2. Properties
|
|
19
|
-
#
|
|
20
|
-
# | @data@ | A hash of this metadata's contents (YAML serialized in the database). |
|
|
21
|
-
|
|
22
|
-
class Metadata < HasMetadata::Model
|
|
23
|
-
end
|
|
24
|
-
</code></pre>
|
|
25
|
-
|
|
26
|
-
Note that this is significantly emptier than the previous model.
|
|
27
|
-
|
|
28
|
-
h2. About
|
|
29
|
-
|
|
30
|
-
Wide tables are a problem for big databases. If your @ActiveRecord@ models have
|
|
31
|
-
10, maybe 15 columns, some of which are @VARCHARs@ or maybe even @TEXTs@, it's
|
|
32
|
-
going to slow your queries down when you start to scale up.
|
|
33
|
-
|
|
34
|
-
The easy solution to this problem is to limit your projections; in other words,
|
|
35
|
-
to only @SELECT@ the columns that you actually need. If you've got a @users@
|
|
36
|
-
table with a giant @about_me@ text column, and you're only trying to look up the
|
|
37
|
-
user's login, then just select the @login@ column.
|
|
38
|
-
|
|
39
|
-
In the long run, though, a superior solution is to just move those
|
|
40
|
-
@about_me@-type columns to a completely different table. This table has just one
|
|
41
|
-
JSON-serialized field, making it schemaless, so it doesn't waste space. Each row
|
|
42
|
-
in this table is associated with a record in another table (@Metadata@ @has_one@
|
|
43
|
-
of your models).
|
|
44
|
-
|
|
45
|
-
This way, when your website gets huge, all of your giant, freeform data is in
|
|
46
|
-
one table that you can shard, or move off to an alternate database, or even a
|
|
47
|
-
NoSQL-type document store, or otherwise manage as you please. Your relational
|
|
48
|
-
tables remain slim and efficient, containing only columns that a) are indexed,
|
|
49
|
-
or b) you need frequent access to.
|
|
50
|
-
|
|
51
|
-
This gem includes a generator that creates the @Metadata@ model, and a module
|
|
52
|
-
that you can include in your models to define which fields have been spun off to
|
|
53
|
-
the metadata record.
|
|
54
|
-
|
|
55
|
-
h2. Installation
|
|
56
|
-
|
|
57
|
-
*Important Note:* This gem is only compatible with Ruby 1.9 and Rails 3.0.
|
|
58
|
-
|
|
59
|
-
Firstly, add the gem to your Rails project's @Gemfile@:
|
|
60
|
-
|
|
61
|
-
<pre><code>
|
|
62
|
-
gem 'has_metadata'
|
|
63
|
-
</code></pre>
|
|
64
|
-
|
|
65
|
-
Next, run the generator, which will add the @Metadata@ model and its migration
|
|
66
|
-
to your application.
|
|
67
|
-
|
|
68
|
-
<pre><code>
|
|
69
|
-
rails generate metadata
|
|
70
|
-
</code></pre>
|
|
71
|
-
|
|
72
|
-
h2. Usage
|
|
73
|
-
|
|
74
|
-
The first thing to think about is what columns to keep in your model. You will
|
|
75
|
-
need to keep any indexed columns, or any columns you perform lookups or other
|
|
76
|
-
SQL queries with. You should also keep any frequently accessed columns,
|
|
77
|
-
especially if they are small (integers or booleans). Good candidates for the
|
|
78
|
-
metadata table are the @TEXT@- and @VARCHAR@-type columns that you only need to
|
|
79
|
-
render a page or two in your app.
|
|
80
|
-
|
|
81
|
-
You'll need to change your model's schema so that it has a @metadata_id@ column
|
|
82
|
-
that will associate the model with its @Metadata@ instance:
|
|
83
|
-
|
|
84
|
-
<pre><code>
|
|
85
|
-
t.belongs_to :metadata
|
|
86
|
-
</code></pre>
|
|
87
|
-
|
|
88
|
-
Next, include the @HasMetadata@ module in your model, and call the
|
|
89
|
-
@has_metadata@ method to define the schema of your metadata. You can get more
|
|
90
|
-
information in the {HasMetadata::ClassMethods#has_metadata} documentation, but for starters, here's a
|
|
91
|
-
basic example:
|
|
92
|
-
|
|
93
|
-
<pre><code>
|
|
94
|
-
class User < ActiveRecord::Base
|
|
95
|
-
include HasMetadata
|
|
96
|
-
has_metadata({
|
|
97
|
-
about_me: { type: String, length: { maximum: 512 } },
|
|
98
|
-
birthdate: { type: Date, presence: true },
|
|
99
|
-
zipcode: { type: Number, numericality: { greater_than: 9999, less_than: 10_000_} }
|
|
100
|
-
})
|
|
101
|
-
end
|
|
102
|
-
</code></pre>
|
|
103
|
-
|
|
104
|
-
As you can see, you pass field names mapped to a hash. The hash describes the
|
|
105
|
-
validation that will be performed, and is in the same format as a call to
|
|
106
|
-
@validates@. In addition to the @EachValidator@ keys shown above, you can also
|
|
107
|
-
pass a @type@ key, to constrain the Ruby type that can be assigned to the field.
|
|
108
|
-
|
|
109
|
-
Each of these fields (in this case, @about_me@, @birthdate@, and @zipcode@) can
|
|
110
|
-
be accessed and set as first_level methods on an instance of your model:
|
|
111
|
-
|
|
112
|
-
<pre><code>
|
|
113
|
-
user.about_me #=> "I was born in 1982 in Aberdeen. My father was a carpenter from..."
|
|
114
|
-
</code></pre>
|
|
115
|
-
|
|
116
|
-
... and thus, used as part of @form_for@ fields:
|
|
117
|
-
|
|
118
|
-
<pre><code>
|
|
119
|
-
form_for user do |f|
|
|
120
|
-
f.text_area :about_me, rows: 5, cols: 80
|
|
121
|
-
end
|
|
122
|
-
</code></pre>
|
|
123
|
-
|
|
124
|
-
The only thing you _can't_ do is use these fields in a query, obviously. You
|
|
125
|
-
can't do something like @User.where(zipcode: 90210)@, because that column
|
|
126
|
-
doesn't exist on the @users@ table.
|