rails_lookup 0.0.1
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/Manifest +4 -0
- data/README +139 -0
- data/Rakefile +29 -0
- data/lib/active_record/lookup.rb +134 -0
- data/rails_lookup.gemspec +168 -0
- metadata +112 -0
data/Manifest
ADDED
data/README
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
Lookup tables with ruby-on-rails
|
2
|
+
--------------------------------
|
3
|
+
|
4
|
+
By: Nimrod Priell
|
5
|
+
|
6
|
+
This gem adds an ActiveRecord macro to define memory-cached, dynamically growing, normalized lookup tables for entity 'type'-like objects. Or in plain English - if you want to have a table containing, say, ProductTypes which can grow with new types simply when you refer to them, and not keep the Product table containing a thousand repeating 'type="book"' entries - sit down and try to follow through.
|
7
|
+
|
8
|
+
Motivation
|
9
|
+
----------
|
10
|
+
|
11
|
+
A [normalized DB][1] means that you want to keep types as separate tables, with foreign keys pointing from your main entity to its type. For instance, instead of
|
12
|
+
ID | car_name | car_type
|
13
|
+
1 | Chevrolet Aveo | Compact
|
14
|
+
2 | Ford Fiesta | Compact
|
15
|
+
3 | BMW Z-5 | Sports
|
16
|
+
|
17
|
+
You want to have two tables:
|
18
|
+
ID | car_name | car_type_id
|
19
|
+
1 | Chevrolet Aveo | 1
|
20
|
+
2 | Ford Fiesta | 1
|
21
|
+
3 | BMW Z-5 | 2
|
22
|
+
|
23
|
+
And
|
24
|
+
|
25
|
+
car_type_id | car_type_name
|
26
|
+
1 | Compact
|
27
|
+
2 | Sports
|
28
|
+
|
29
|
+
The pros/cons of a normalized DB can be discussed elsewhere. I'd just point out a denormalized solution is most useful in settings like [column oriented DBMSes][2]. For the rest of us folks using standard databases, we usually want to use lookups.
|
30
|
+
|
31
|
+
The usual way to do this with ruby on rails is
|
32
|
+
* Generate a CarType model using `rails generate model CarType name:string`
|
33
|
+
* Link between CarType and Car tables using `belongs_to` and `has_many`
|
34
|
+
|
35
|
+
Then to work with this you can transparently read the car type:
|
36
|
+
|
37
|
+
car = Car.all.first
|
38
|
+
car.car_type.name # returns "Compact"
|
39
|
+
|
40
|
+
Ruby does an awesome job of caching the results for you, so that you'll probably not hit the DB every time you get the same car type from different car objects.
|
41
|
+
|
42
|
+
You can even make this shorter, by defining a delegate to car_type_name from CarType:
|
43
|
+
|
44
|
+
*in car_type_name.rb*
|
45
|
+
|
46
|
+
delegate :name, :to => :car, :prefix => true
|
47
|
+
|
48
|
+
And now you can access this as
|
49
|
+
|
50
|
+
car.car_type_name
|
51
|
+
|
52
|
+
However, it's less pleasant to insert with this technique:
|
53
|
+
|
54
|
+
car.car_type.car_type_name = "Sports"
|
55
|
+
car.car_type.save!
|
56
|
+
#Now let's see what happened to the OTHER compact car
|
57
|
+
Car.all.second.car_type_name #Oops, returns "Sports"
|
58
|
+
|
59
|
+
Right, what are we doing? We should've used
|
60
|
+
|
61
|
+
car.update_attributes(car_type: CarType.find_or_create_by_name(name: "Sports"))
|
62
|
+
|
63
|
+
Okay. Probably want to shove that into its own method rather than have this repeated in the code several times. But you also need a helper method for creating cars that way…
|
64
|
+
|
65
|
+
Furthermore, ruby is good about caching, but it caches by the exact query used, and the cache expires after the controller action ends. You can configure more advanced caches, perhaps.
|
66
|
+
|
67
|
+
The thing is all this can get tedious if you use a normalized structure where you have 15 entities and each has at least one 'type-like' field. That's a whole lot of dangling Type objects. What you really want is an interface like this:
|
68
|
+
|
69
|
+
car.all.first
|
70
|
+
car.car_type #return "Compact"
|
71
|
+
car.car_type = "Sports" #No effect on car.all.second, just automatically use the second constant
|
72
|
+
car.car_type = "Sedan" #Magically create a new type
|
73
|
+
|
74
|
+
Oh, and it'll be nice if all of this is cached and you can define car types as constants (or symbols). You obviously still want to be able to run:
|
75
|
+
|
76
|
+
CarType.where (:id > 3) #Just an example of supposed "arbitrary" SQL involving a real live CarType class
|
77
|
+
|
78
|
+
But you wanna minimize generating these numerous type classes. If you're like me, you don't even want to see them lying around in app/model. Who cares about them?
|
79
|
+
|
80
|
+
I've looked thoroughly for a nice rails solution to this, but after failing to find one, I created my own rails metaprogramming hook.
|
81
|
+
|
82
|
+
Installation
|
83
|
+
------------
|
84
|
+
|
85
|
+
The result of this hook is that you get the exact syntax described above, with only two lines of code (no extra classes or anything):
|
86
|
+
In your ActiveRecord object simply add
|
87
|
+
|
88
|
+
require 'active_record/lookup'
|
89
|
+
class Car < ActiveRecord::Base
|
90
|
+
#...
|
91
|
+
include ActiveRecord::Lookup
|
92
|
+
lookup :car_type
|
93
|
+
#...
|
94
|
+
end
|
95
|
+
|
96
|
+
That's it. the generated CarType class (which you won't see as a car_type.rb file, obviously, as it is generated in real-time), contains some nice methods to look into the cache as well: So you can call
|
97
|
+
|
98
|
+
CarType.id_for "Sports" #Returns 2
|
99
|
+
CarType.name_for 1 #Returns "Compact"
|
100
|
+
|
101
|
+
and you can still hack at the underlying ID for an object, if you need to:
|
102
|
+
|
103
|
+
car = car.all.first
|
104
|
+
car.car_type = "Sports"
|
105
|
+
car.car_type_id #Returns 2
|
106
|
+
car.car_type_id = 1
|
107
|
+
car.car_type #Returns "Compact"
|
108
|
+
|
109
|
+
The only remaining thing is to define your migrations for creating the actual database tables. After all, that's something you only want to do once and not every time this class loads, so this isn't the place for it. However, it's easy enough to create your own scaffolds so that
|
110
|
+
|
111
|
+
rails generate migration create_car_type_lookup_for_car
|
112
|
+
|
113
|
+
will automatically create the migration
|
114
|
+
|
115
|
+
class CreateCarTypeLookupForCar < ActiveRecord::Migration
|
116
|
+
def self.up
|
117
|
+
create_table :car_types do |t|
|
118
|
+
t.string :name
|
119
|
+
t.timestamps #Btw you can remove these, I don't much like them in type tables anyway
|
120
|
+
end
|
121
|
+
|
122
|
+
remove_column :cars, :car_type #Let's assume you have one of those now…
|
123
|
+
add_column :cars, :car_type_id, :integer #Maybe put not_null constraints here.
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.down
|
127
|
+
drop_table :car_types
|
128
|
+
add_column :cars, :car_type, :string
|
129
|
+
remove_column :cars, :car_type_id
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
I'll let you work out the details for actually migrating the data yourself.
|
134
|
+
|
135
|
+
[1] http://en.wikipedia.org/wiki/Database_normalization
|
136
|
+
[2] http://en.wikipedia.org/wiki/Column-oriented_DBMS
|
137
|
+
|
138
|
+
I hope this helped you and saved a lot of time and frustration. Follow me on twitter: @nimrodpriell
|
139
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'echoe'
|
4
|
+
|
5
|
+
Echoe.new('rails_lookup', '0.0.1') do |s|
|
6
|
+
s.description = File.read(File.join(File.dirname(__FILE__), 'README'))
|
7
|
+
s.summary = "Lookup table macro for ActiveRecords"
|
8
|
+
s.url = "http://github.com/Nimster/RailsLookup/"
|
9
|
+
s.author = "Nimrod Priell"
|
10
|
+
s.email = "@nimrodpriell" #Twitter
|
11
|
+
s.ignore_pattern = ["tmp/*", "script/*"]
|
12
|
+
s.development_dependencies = []
|
13
|
+
end
|
14
|
+
|
15
|
+
Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
|
16
|
+
=begin
|
17
|
+
spec = Gem::Specification.new do |s|
|
18
|
+
s.name = "rails_lookup"
|
19
|
+
s.requirements = [ 'Rails >= 3.0.7, Ruby >= 1.9.1' ]
|
20
|
+
s.version = "0.0.1"
|
21
|
+
s.homepage = ""
|
22
|
+
s.platform = "Gem::Platform::RUBY"
|
23
|
+
s.required_ruby_version = '>=1.9'
|
24
|
+
s.files = Dir['**/**']
|
25
|
+
s.executables = []
|
26
|
+
s.test_files = [] #Dir["test/test*.rb"]
|
27
|
+
s.has_rdoc = false
|
28
|
+
end
|
29
|
+
=end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# Author: Nimrod Priell (Twitter: @nimrodpriell)
|
2
|
+
#
|
3
|
+
# See README file or blog post for more.
|
4
|
+
# This is intended to be included into ActiveRecord entities: Simply
|
5
|
+
# include ActiveRecord::Lookup
|
6
|
+
# in your class that extends ActiveRecord::Base
|
7
|
+
module ActiveRecord
|
8
|
+
module Lookup
|
9
|
+
#These will be defined as class methods.
|
10
|
+
module ClassMethods
|
11
|
+
#We define only this "macro". In your ActiveRecord entity (say, Car), use
|
12
|
+
# lookup :car_type
|
13
|
+
# which will create Car#car_type, Car#car_type=, Car#car_type_id,
|
14
|
+
# Car#car_type_id= and CarType, an ActiveRecord with a name (String)
|
15
|
+
# attribute, that has_many :cars.
|
16
|
+
#
|
17
|
+
# You can also use
|
18
|
+
# lookup :car_type, :as => :type
|
19
|
+
# which will do the same thing but create Car#type, Car#type=, Car#type_id
|
20
|
+
# and Car#type_id= instead.
|
21
|
+
def lookup(lookup_name, opts = {})
|
22
|
+
as_name = opts[:as] || lookup_name
|
23
|
+
mycls = self #Class I'm defined in
|
24
|
+
|
25
|
+
#Create the new ActiveRecord for the lookup
|
26
|
+
cls = Class.new(ActiveRecord::Base) do
|
27
|
+
#It has_many of the containing class
|
28
|
+
has_many mycls.to_s.split(/[A-Z]/).map(&:downcase).join('_').to_sym
|
29
|
+
|
30
|
+
validates_uniqueness_of :name
|
31
|
+
validates :name, :presence => true
|
32
|
+
|
33
|
+
#Query the cache for the DB ID of a certain value
|
34
|
+
def self.id_for(name, id = nil)
|
35
|
+
class_variable_get(:@@rcaches)[name] ||= id
|
36
|
+
end
|
37
|
+
|
38
|
+
#This helper method is the "find_or_create" of the class that also
|
39
|
+
#updates the DB. I didn't want to overwrite the original
|
40
|
+
#find_or_create_by_name since it is linked with a lot of the rails
|
41
|
+
#behaviour and I want to avoid altering that.
|
42
|
+
def self.gen_id_for(val)
|
43
|
+
id = id_for val
|
44
|
+
if id.nil?
|
45
|
+
#Define this new possible value
|
46
|
+
new_db_obj = find_or_create_by_name val #You could just override this...
|
47
|
+
id_for val, new_db_obj.id
|
48
|
+
name_for new_db_obj.id, val
|
49
|
+
id = new_db_obj.id
|
50
|
+
end
|
51
|
+
id
|
52
|
+
end
|
53
|
+
|
54
|
+
#Query the cache for the value that goes with a certain DB ID
|
55
|
+
def self.name_for(id, name = nil)
|
56
|
+
class_variable_get(:@@caches)[id] ||= name
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
#Bind the created class to a name
|
61
|
+
lookup_cls_name = lookup_name.to_s.split("_").map(&:capitalize).join("")
|
62
|
+
Object.const_set lookup_cls_name, cls #Define it as a global class
|
63
|
+
|
64
|
+
#Define the setter by string value
|
65
|
+
define_method("#{as_name.to_s}=") do |val|
|
66
|
+
id = cls.gen_id_for val
|
67
|
+
write_attribute "#{as_name.to_s}_id".to_sym, id
|
68
|
+
end
|
69
|
+
|
70
|
+
#Define the getter
|
71
|
+
define_method("#{as_name.to_s}") do
|
72
|
+
id = read_attribute "@#{as_name.to_s}_id".to_sym
|
73
|
+
if not id.nil?
|
74
|
+
value = cls.name_for id
|
75
|
+
if value.nil?
|
76
|
+
lookup_obj = cls.find_by_id id
|
77
|
+
if not lookup_obj.nil?
|
78
|
+
cls.name_for id, lookup_obj.name
|
79
|
+
cls.id_for lookup_obj.name, id
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
value
|
84
|
+
end
|
85
|
+
|
86
|
+
#This sucks but it's the only way I could figure out to create the exact
|
87
|
+
#method_missing method signature and still pass it the values I need to
|
88
|
+
#use it. Maybe these should be class variables, but the best thing is if
|
89
|
+
#they could only be defined in this scope and carried along with the
|
90
|
+
#closure to the actual method.
|
91
|
+
@as_name = as_name
|
92
|
+
@cls = cls
|
93
|
+
#We need to wrap around rails' default finders so that
|
94
|
+
#find_by_car_type and similar methods will behave correctly. We translate
|
95
|
+
#them on the fly to find_by_car_type_id and give the id instead of the
|
96
|
+
#requested name.
|
97
|
+
def method_missing(method_id, *arguments, &block)
|
98
|
+
if match = ActiveRecord::DynamicFinderMatch.match(method_id)
|
99
|
+
#reroute car_type to _car_type_id
|
100
|
+
idx = match.attribute_names.find_index { |n| n == @as_name.to_s }
|
101
|
+
if not idx.nil?
|
102
|
+
self.new.send "#{@as_name}=", arguments[idx] #Make sure there's a cached value
|
103
|
+
arguments[idx] = @cls.id_for arguments[idx] #Change the argument
|
104
|
+
method_id = method_id.to_s.sub /(by|and)_#{@as_name}$/, "\\1_#{@as_name}_id"
|
105
|
+
method_id = method_id.to_s.sub /(by|and)_#{@as_name}_and/, "\\1_#{@as_name}_id_and"
|
106
|
+
method_id = method_id.to_sym
|
107
|
+
end
|
108
|
+
end
|
109
|
+
super
|
110
|
+
end
|
111
|
+
|
112
|
+
#Define the link between the host class and the newly created ActiveRecord
|
113
|
+
belongs_to lookup_name.to_s.to_sym, :foreign_key => "#{as_name}_id".to_sym
|
114
|
+
validates "#{as_name.to_s}_id".to_sym, :presence => true
|
115
|
+
|
116
|
+
#Prefill the hashes from the DB
|
117
|
+
all_vals = cls.all
|
118
|
+
cls.class_variable_set(:@@rcaches, all_vals.inject({}) do |r, obj|
|
119
|
+
r[obj.name] = obj.id
|
120
|
+
r
|
121
|
+
end)
|
122
|
+
cls.class_variable_set(:@@caches, all_vals.inject([]) do |r, obj|
|
123
|
+
r[obj.id] = obj.name
|
124
|
+
r
|
125
|
+
end)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# extend host class with class methods when we're included
|
130
|
+
def self.included(host_class)
|
131
|
+
host_class.extend(ClassMethods)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{rails_lookup}
|
5
|
+
s.version = "0.0.1"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = [%q{Nimrod Priell}]
|
9
|
+
s.date = %q{2011-08-06}
|
10
|
+
s.description = %q{Lookup tables with ruby-on-rails
|
11
|
+
--------------------------------
|
12
|
+
|
13
|
+
By: Nimrod Priell
|
14
|
+
|
15
|
+
This gem adds an ActiveRecord macro to define memory-cached, dynamically growing, normalized lookup tables for entity 'type'-like objects. Or in plain English - if you want to have a table containing, say, ProductTypes which can grow with new types simply when you refer to them, and not keep the Product table containing a thousand repeating 'type="book"' entries - sit down and try to follow through.
|
16
|
+
|
17
|
+
Motivation
|
18
|
+
----------
|
19
|
+
|
20
|
+
A [normalized DB][1] means that you want to keep types as separate tables, with foreign keys pointing from your main entity to its type. For instance, instead of
|
21
|
+
ID | car_name | car_type
|
22
|
+
1 | Chevrolet Aveo | Compact
|
23
|
+
2 | Ford Fiesta | Compact
|
24
|
+
3 | BMW Z-5 | Sports
|
25
|
+
|
26
|
+
You want to have two tables:
|
27
|
+
ID | car_name | car_type_id
|
28
|
+
1 | Chevrolet Aveo | 1
|
29
|
+
2 | Ford Fiesta | 1
|
30
|
+
3 | BMW Z-5 | 2
|
31
|
+
|
32
|
+
And
|
33
|
+
|
34
|
+
car_type_id | car_type_name
|
35
|
+
1 | Compact
|
36
|
+
2 | Sports
|
37
|
+
|
38
|
+
The pros/cons of a normalized DB can be discussed elsewhere. I'd just point out a denormalized solution is most useful in settings like [column oriented DBMSes][2]. For the rest of us folks using standard databases, we usually want to use lookups.
|
39
|
+
|
40
|
+
The usual way to do this with ruby on rails is
|
41
|
+
* Generate a CarType model using `rails generate model CarType name:string`
|
42
|
+
* Link between CarType and Car tables using `belongs_to` and `has_many`
|
43
|
+
|
44
|
+
Then to work with this you can transparently read the car type:
|
45
|
+
|
46
|
+
car = Car.all.first
|
47
|
+
car.car_type.name # returns "Compact"
|
48
|
+
|
49
|
+
Ruby does an awesome job of caching the results for you, so that you'll probably not hit the DB every time you get the same car type from different car objects.
|
50
|
+
|
51
|
+
You can even make this shorter, by defining a delegate to car_type_name from CarType:
|
52
|
+
|
53
|
+
*in car_type_name.rb*
|
54
|
+
|
55
|
+
delegate :name, :to => :car, :prefix => true
|
56
|
+
|
57
|
+
And now you can access this as
|
58
|
+
|
59
|
+
car.car_type_name
|
60
|
+
|
61
|
+
However, it's less pleasant to insert with this technique:
|
62
|
+
|
63
|
+
car.car_type.car_type_name = "Sports"
|
64
|
+
car.car_type.save!
|
65
|
+
#Now let's see what happened to the OTHER compact car
|
66
|
+
Car.all.second.car_type_name #Oops, returns "Sports"
|
67
|
+
|
68
|
+
Right, what are we doing? We should've used
|
69
|
+
|
70
|
+
car.update_attributes(car_type: CarType.find_or_create_by_name(name: "Sports"))
|
71
|
+
|
72
|
+
Okay. Probably want to shove that into its own method rather than have this repeated in the code several times. But you also need a helper method for creating cars that way…
|
73
|
+
|
74
|
+
Furthermore, ruby is good about caching, but it caches by the exact query used, and the cache expires after the controller action ends. You can configure more advanced caches, perhaps.
|
75
|
+
|
76
|
+
The thing is all this can get tedious if you use a normalized structure where you have 15 entities and each has at least one 'type-like' field. That's a whole lot of dangling Type objects. What you really want is an interface like this:
|
77
|
+
|
78
|
+
car.all.first
|
79
|
+
car.car_type #return "Compact"
|
80
|
+
car.car_type = "Sports" #No effect on car.all.second, just automatically use the second constant
|
81
|
+
car.car_type = "Sedan" #Magically create a new type
|
82
|
+
|
83
|
+
Oh, and it'll be nice if all of this is cached and you can define car types as constants (or symbols). You obviously still want to be able to run:
|
84
|
+
|
85
|
+
CarType.where (:id > 3) #Just an example of supposed "arbitrary" SQL involving a real live CarType class
|
86
|
+
|
87
|
+
But you wanna minimize generating these numerous type classes. If you're like me, you don't even want to see them lying around in app/model. Who cares about them?
|
88
|
+
|
89
|
+
I've looked thoroughly for a nice rails solution to this, but after failing to find one, I created my own rails metaprogramming hook.
|
90
|
+
|
91
|
+
Installation
|
92
|
+
------------
|
93
|
+
|
94
|
+
The result of this hook is that you get the exact syntax described above, with only two lines of code (no extra classes or anything):
|
95
|
+
In your ActiveRecord object simply add
|
96
|
+
|
97
|
+
require 'active_record/lookup'
|
98
|
+
class Car < ActiveRecord::Base
|
99
|
+
#...
|
100
|
+
include ActiveRecord::Lookup
|
101
|
+
lookup :car_type
|
102
|
+
#...
|
103
|
+
end
|
104
|
+
|
105
|
+
That's it. the generated CarType class (which you won't see as a car_type.rb file, obviously, as it is generated in real-time), contains some nice methods to look into the cache as well: So you can call
|
106
|
+
|
107
|
+
CarType.id_for "Sports" #Returns 2
|
108
|
+
CarType.name_for 1 #Returns "Compact"
|
109
|
+
|
110
|
+
and you can still hack at the underlying ID for an object, if you need to:
|
111
|
+
|
112
|
+
car = car.all.first
|
113
|
+
car.car_type = "Sports"
|
114
|
+
car.car_type_id #Returns 2
|
115
|
+
car.car_type_id = 1
|
116
|
+
car.car_type #Returns "Compact"
|
117
|
+
|
118
|
+
The only remaining thing is to define your migrations for creating the actual database tables. After all, that's something you only want to do once and not every time this class loads, so this isn't the place for it. However, it's easy enough to create your own scaffolds so that
|
119
|
+
|
120
|
+
rails generate migration create_car_type_lookup_for_car
|
121
|
+
|
122
|
+
will automatically create the migration
|
123
|
+
|
124
|
+
class CreateCarTypeLookupForCar < ActiveRecord::Migration
|
125
|
+
def self.up
|
126
|
+
create_table :car_types do |t|
|
127
|
+
t.string :name
|
128
|
+
t.timestamps #Btw you can remove these, I don't much like them in type tables anyway
|
129
|
+
end
|
130
|
+
|
131
|
+
remove_column :cars, :car_type #Let's assume you have one of those now…
|
132
|
+
add_column :cars, :car_type_id, :integer #Maybe put not_null constraints here.
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.down
|
136
|
+
drop_table :car_types
|
137
|
+
add_column :cars, :car_type, :string
|
138
|
+
remove_column :cars, :car_type_id
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
I'll let you work out the details for actually migrating the data yourself.
|
143
|
+
|
144
|
+
[1] http://en.wikipedia.org/wiki/Database_normalization
|
145
|
+
[2] http://en.wikipedia.org/wiki/Column-oriented_DBMS
|
146
|
+
|
147
|
+
I hope this helped you and saved a lot of time and frustration. Follow me on twitter: @nimrodpriell
|
148
|
+
|
149
|
+
}
|
150
|
+
s.email = %q{@nimrodpriell}
|
151
|
+
s.extra_rdoc_files = [%q{README}, %q{lib/active_record/lookup.rb}]
|
152
|
+
s.files = [%q{README}, %q{Rakefile}, %q{lib/active_record/lookup.rb}, %q{Manifest}, %q{rails_lookup.gemspec}]
|
153
|
+
s.homepage = %q{http://github.com/Nimster/RailsLookup/}
|
154
|
+
s.rdoc_options = [%q{--line-numbers}, %q{--inline-source}, %q{--title}, %q{Rails_lookup}, %q{--main}, %q{README}]
|
155
|
+
s.require_paths = [%q{lib}]
|
156
|
+
s.rubyforge_project = %q{rails_lookup}
|
157
|
+
s.rubygems_version = %q{1.8.7}
|
158
|
+
s.summary = %q{Lookup table macro for ActiveRecords}
|
159
|
+
|
160
|
+
if s.respond_to? :specification_version then
|
161
|
+
s.specification_version = 3
|
162
|
+
|
163
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
164
|
+
else
|
165
|
+
end
|
166
|
+
else
|
167
|
+
end
|
168
|
+
end
|
metadata
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rails_lookup
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Nimrod Priell
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-08-06 00:00:00 Z
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: "Lookup tables with ruby-on-rails\n\
|
17
|
+
--------------------------------\n\n\
|
18
|
+
By: Nimrod Priell\n\n\
|
19
|
+
This gem adds an ActiveRecord macro to define memory-cached, dynamically growing, normalized lookup tables for entity 'type'-like objects. Or in plain English - if you want to have a table containing, say, ProductTypes which can grow with new types simply when you refer to them, and not keep the Product table containing a thousand repeating 'type=\"book\"' entries - sit down and try to follow through.\n\n\
|
20
|
+
Motivation\n\
|
21
|
+
----------\n\n\
|
22
|
+
A [normalized DB][1] means that you want to keep types as separate tables, with foreign keys pointing from your main entity to its type. For instance, instead of \n\
|
23
|
+
ID | car_name | car_type\n\
|
24
|
+
1 | Chevrolet Aveo | Compact\n\
|
25
|
+
2 | Ford Fiesta | Compact\n\
|
26
|
+
3 | BMW Z-5 | Sports\n\n\
|
27
|
+
You want to have two tables:\n\
|
28
|
+
ID | car_name | car_type_id\n\
|
29
|
+
1 | Chevrolet Aveo | 1\n\
|
30
|
+
2 | Ford Fiesta | 1\n\
|
31
|
+
3 | BMW Z-5 | 2\n\n\
|
32
|
+
And\n\n\
|
33
|
+
car_type_id | car_type_name \n\
|
34
|
+
1 | Compact\n\
|
35
|
+
2 | Sports\n\n\
|
36
|
+
The pros/cons of a normalized DB can be discussed elsewhere. I'd just point out a denormalized solution is most useful in settings like [column oriented DBMSes][2]. For the rest of us folks using standard databases, we usually want to use lookups.\n\n\
|
37
|
+
The usual way to do this with ruby on rails is\n\
|
38
|
+
* Generate a CarType model using `rails generate model CarType name:string`\n\
|
39
|
+
* Link between CarType and Car tables using `belongs_to` and `has_many`\n\n\
|
40
|
+
Then to work with this you can transparently read the car type:\n\n car = Car.all.first\n car.car_type.name # returns \"Compact\"\n\n\
|
41
|
+
Ruby does an awesome job of caching the results for you, so that you'll probably not hit the DB every time you get the same car type from different car objects.\n\n\
|
42
|
+
You can even make this shorter, by defining a delegate to car_type_name from CarType:\n\n\
|
43
|
+
*in car_type_name.rb*\n \n delegate :name, :to => :car, :prefix => true\n\n\
|
44
|
+
And now you can access this as \n\n car.car_type_name\n\n\
|
45
|
+
However, it's less pleasant to insert with this technique:\n\n car.car_type.car_type_name = \"Sports\"\n car.car_type.save!\n #Now let's see what happened to the OTHER compact car\n Car.all.second.car_type_name #Oops, returns \"Sports\"\n\n\
|
46
|
+
Right, what are we doing? We should've used\n\n car.update_attributes(car_type: CarType.find_or_create_by_name(name: \"Sports\"))\n\n\
|
47
|
+
Okay. Probably want to shove that into its own method rather than have this repeated in the code several times. But you also need a helper method for creating cars that way\xE2\x80\xA6\n\n\
|
48
|
+
Furthermore, ruby is good about caching, but it caches by the exact query used, and the cache expires after the controller action ends. You can configure more advanced caches, perhaps.\n\n\
|
49
|
+
The thing is all this can get tedious if you use a normalized structure where you have 15 entities and each has at least one 'type-like' field. That's a whole lot of dangling Type objects. What you really want is an interface like this:\n\n car.all.first\n car.car_type #return \"Compact\"\n car.car_type = \"Sports\" #No effect on car.all.second, just automatically use the second constant\n car.car_type = \"Sedan\" #Magically create a new type\n\n\
|
50
|
+
Oh, and it'll be nice if all of this is cached and you can define car types as constants (or symbols). You obviously still want to be able to run:\n\n CarType.where (:id > 3) #Just an example of supposed \"arbitrary\" SQL involving a real live CarType class\n\n\
|
51
|
+
But you wanna minimize generating these numerous type classes. If you're like me, you don't even want to see them lying around in app/model. Who cares about them?\n\n\
|
52
|
+
I've looked thoroughly for a nice rails solution to this, but after failing to find one, I created my own rails metaprogramming hook.\n\n\
|
53
|
+
Installation\n\
|
54
|
+
------------\n\n\
|
55
|
+
The result of this hook is that you get the exact syntax described above, with only two lines of code (no extra classes or anything):\n\
|
56
|
+
In your ActiveRecord object simply add\n\n require 'active_record/lookup'\n class Car < ActiveRecord::Base\n #...\n include ActiveRecord::Lookup\n lookup :car_type\n #...\n end\n\n\
|
57
|
+
That's it. the generated CarType class (which you won't see as a car_type.rb file, obviously, as it is generated in real-time), contains some nice methods to look into the cache as well: So you can call\n\n CarType.id_for \"Sports\" #Returns 2\n CarType.name_for 1 #Returns \"Compact\"\n\n\
|
58
|
+
and you can still hack at the underlying ID for an object, if you need to:\n\n car = car.all.first\n car.car_type = \"Sports\"\n car.car_type_id #Returns 2\n car.car_type_id = 1\n car.car_type #Returns \"Compact\"\n\n\
|
59
|
+
The only remaining thing is to define your migrations for creating the actual database tables. After all, that's something you only want to do once and not every time this class loads, so this isn't the place for it. However, it's easy enough to create your own scaffolds so that \n\n rails generate migration create_car_type_lookup_for_car\n\n\
|
60
|
+
will automatically create the migration\n\n class CreateCarTypeLookupForCar < ActiveRecord::Migration\n def self.up\n create_table :car_types do |t|\n t.string :name\n t.timestamps #Btw you can remove these, I don't much like them in type tables anyway\n end\n \n remove_column :cars, :car_type #Let's assume you have one of those now\xE2\x80\xA6\n add_column :cars, :car_type_id, :integer #Maybe put not_null constraints here.\n end\n\n def self.down\n drop_table :car_types\n add_column :cars, :car_type, :string\n remove_column :cars, :car_type_id\n end\n end\n\n\
|
61
|
+
I'll let you work out the details for actually migrating the data yourself. \n\n\
|
62
|
+
[1] http://en.wikipedia.org/wiki/Database_normalization\n\
|
63
|
+
[2] http://en.wikipedia.org/wiki/Column-oriented_DBMS\n\n\
|
64
|
+
I hope this helped you and saved a lot of time and frustration. Follow me on twitter: @nimrodpriell\n\n"
|
65
|
+
email: "@nimrodpriell"
|
66
|
+
executables: []
|
67
|
+
|
68
|
+
extensions: []
|
69
|
+
|
70
|
+
extra_rdoc_files:
|
71
|
+
- README
|
72
|
+
- lib/active_record/lookup.rb
|
73
|
+
files:
|
74
|
+
- README
|
75
|
+
- Rakefile
|
76
|
+
- lib/active_record/lookup.rb
|
77
|
+
- Manifest
|
78
|
+
- rails_lookup.gemspec
|
79
|
+
homepage: http://github.com/Nimster/RailsLookup/
|
80
|
+
licenses: []
|
81
|
+
|
82
|
+
post_install_message:
|
83
|
+
rdoc_options:
|
84
|
+
- --line-numbers
|
85
|
+
- --inline-source
|
86
|
+
- --title
|
87
|
+
- Rails_lookup
|
88
|
+
- --main
|
89
|
+
- README
|
90
|
+
require_paths:
|
91
|
+
- lib
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
none: false
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: "0"
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: "1.2"
|
104
|
+
requirements: []
|
105
|
+
|
106
|
+
rubyforge_project: rails_lookup
|
107
|
+
rubygems_version: 1.8.7
|
108
|
+
signing_key:
|
109
|
+
specification_version: 3
|
110
|
+
summary: Lookup table macro for ActiveRecords
|
111
|
+
test_files: []
|
112
|
+
|