rails_lookup 0.0.3 → 0.0.4
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/.gitignore +9 -0
- data/Gemfile +4 -0
- data/README +13 -0
- data/Rakefile +8 -29
- data/lib/rails_lookup.rb +163 -0
- data/lib/rails_lookup/version.rb +3 -0
- data/rails_lookup.gemspec +19 -163
- data/test/{test_lookup.rb → lookup_test.rb} +46 -42
- metadata +57 -86
- data/lib/active_record/lookup.rb +0 -153
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README
CHANGED
@@ -140,3 +140,16 @@ I'll let you work out the details for actually migrating the data yourself.
|
|
140
140
|
|
141
141
|
I hope this helped you and saved a lot of time and frustration. Follow me on twitter: @nimrodpriell
|
142
142
|
|
143
|
+
TESTING
|
144
|
+
-------
|
145
|
+
|
146
|
+
rvm gemset create rails_lookup_devel
|
147
|
+
gem install bundler
|
148
|
+
bundle install
|
149
|
+
rake test
|
150
|
+
|
151
|
+
That should basically do it. The gems required are managed in the Gemfile, they
|
152
|
+
are currently Rails and SQLite (which is used only for testing). This has been
|
153
|
+
tested to work with rails-3.0.9
|
154
|
+
|
155
|
+
|
data/Rakefile
CHANGED
@@ -1,29 +1,8 @@
|
|
1
|
-
require '
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
s.author = "Nimrod Priell"
|
10
|
-
s.email = "@nimrodpriell" #Twitter
|
11
|
-
s.ignore_pattern = ["tmp/*", "script/*", "Manifest"]
|
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
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
|
3
|
+
# Adapted from matthewtodd's shoe gem at https://github.com/matthewtodd/shoe
|
4
|
+
desc "Runs all tests for the gem"
|
5
|
+
task :test do
|
6
|
+
spec = Gem::Specification.load(Dir.glob('*.gemspec')[0])
|
7
|
+
system('testrb', *spec.test_files) || exit(1)
|
8
|
+
end
|
data/lib/rails_lookup.rb
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'rails_lookup/version'
|
2
|
+
require 'active_record/base'
|
3
|
+
require 'active_record/relation'
|
4
|
+
|
5
|
+
# Author: Nimrod Priell (Twitter: @nimrodpriell)
|
6
|
+
#
|
7
|
+
# See README file or blog post for more.
|
8
|
+
# This is intended to be included into ActiveRecord entities: Simply
|
9
|
+
# include ActiveRecord::Lookup
|
10
|
+
# in your class that extends ActiveRecord::Base
|
11
|
+
module RailsLookup
|
12
|
+
#These will be defined as class methods so they can be called as "macros"
|
13
|
+
#like belongs_to
|
14
|
+
module ClassMethods
|
15
|
+
#We define only this "macro". In your ActiveRecord entity (say, Car), use
|
16
|
+
# lookup :car_type
|
17
|
+
# which will create Car#car_type, Car#car_type=, Car#car_type_id,
|
18
|
+
# Car#car_type_id= and CarType, an ActiveRecord with a name (String)
|
19
|
+
# attribute, that has_many :cars.
|
20
|
+
#
|
21
|
+
# You can also use
|
22
|
+
# lookup :car_type, :as => :type
|
23
|
+
# which will do the same thing but create Car#type, Car#type=, Car#type_id
|
24
|
+
# and Car#type_id= instead.
|
25
|
+
def lookup(lookup_name, opts = {})
|
26
|
+
as_name = opts[:as] || lookup_name
|
27
|
+
|
28
|
+
#Define the setter for the attribute by textual value
|
29
|
+
mycls = self #Class I'm defined in
|
30
|
+
#Ignore anonymous classes:
|
31
|
+
mycls_sym = mycls.name.tableize.to_sym unless mycls.to_s =~ /#<.*>/
|
32
|
+
lookup_cls_name = lookup_name.to_s.camelize
|
33
|
+
begin
|
34
|
+
#If it exists, just tack on the extra has_many
|
35
|
+
cls = Object.const_get lookup_cls_name
|
36
|
+
cls.has_many mycls_sym unless mycls_sym.blank? #for annon classes
|
37
|
+
rescue NameError
|
38
|
+
#Otherwise, actually define it
|
39
|
+
cls = Class.new(ActiveRecord::Base) do
|
40
|
+
has_many mycls_sym unless mycls_sym.blank? #For anon classes
|
41
|
+
validates_uniqueness_of :name
|
42
|
+
validates :name, :presence => true
|
43
|
+
|
44
|
+
def self.id_for(name, id = nil)
|
45
|
+
class_variable_get(:@@rcaches)[name] ||= id
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.gen_id_for(val)
|
49
|
+
id = id_for val
|
50
|
+
if id.nil?
|
51
|
+
#Define this new possible value
|
52
|
+
#You could just override this...
|
53
|
+
new_db_obj = find_or_create_by_name val
|
54
|
+
id_for val, new_db_obj.id
|
55
|
+
name_for new_db_obj.id, val
|
56
|
+
id = new_db_obj.id
|
57
|
+
end
|
58
|
+
id
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.name_for(id, name = nil)
|
62
|
+
class_variable_get(:@@caches)[id] ||= name
|
63
|
+
end
|
64
|
+
|
65
|
+
#This is especially useful for tests - you want deletion to also
|
66
|
+
#clear the caches otherwise odd stuff might happen
|
67
|
+
def self.delete_all(*args)
|
68
|
+
class_variable_set :@@caches, []
|
69
|
+
class_variable_set :@@rcaches, {}
|
70
|
+
super *args
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
Object.const_set lookup_cls_name, cls #Define it as a global class
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
define_method("#{as_name.to_s}_id=") do |id|
|
79
|
+
write_attribute "#{as_name.to_s}".to_sym, id
|
80
|
+
end
|
81
|
+
|
82
|
+
define_method("#{as_name.to_s}=") do |val|
|
83
|
+
id = cls.gen_id_for val #TODO: Just call #{as_name.to_s}_id=
|
84
|
+
write_attribute "#{as_name.to_s}".to_sym, id
|
85
|
+
end
|
86
|
+
|
87
|
+
define_method("#{as_name.to_s}_id") do
|
88
|
+
read_attribute "#{as_name.to_s}".to_sym
|
89
|
+
end
|
90
|
+
|
91
|
+
#Define the getter
|
92
|
+
define_method("#{as_name.to_s}") do
|
93
|
+
id = read_attribute "#{as_name.to_s}".to_sym
|
94
|
+
if not id.nil?
|
95
|
+
value = cls.name_for id
|
96
|
+
if value.nil?
|
97
|
+
lookup_obj = cls.find_by_id id
|
98
|
+
if not lookup_obj.nil?
|
99
|
+
#TODO: Extract to a method
|
100
|
+
cls.name_for id, lookup_obj.name
|
101
|
+
cls.id_for lookup_obj.name, id
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
value
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
#Rails ActiveRecord support:
|
110
|
+
@cls_for_name = {} if @cls_for_name.nil?
|
111
|
+
@cls_for_name[as_name.to_s.to_sym] = cls
|
112
|
+
|
113
|
+
#This makes find_and_create_by, find_all and where methods all work as
|
114
|
+
#they should. This will not work if you use reset_column_information,
|
115
|
+
#like if you use find_by_session_id in SessionStore. You must redefine
|
116
|
+
#it so it re-sets rel to this singleton object
|
117
|
+
rel = ActiveRecord::Relation.new(self, arel_table)
|
118
|
+
def rel.where(opts, *rest)
|
119
|
+
if opts.is_a? Hash
|
120
|
+
mapped = opts.map do |k,v|
|
121
|
+
if @cls_for_name.has_key?(k.to_sym)
|
122
|
+
[k, @cls_for_name[k.to_sym].id_for(v)]
|
123
|
+
else
|
124
|
+
[k, v]
|
125
|
+
end
|
126
|
+
end
|
127
|
+
opts = Hash[mapped]
|
128
|
+
end
|
129
|
+
super(opts, *rest)
|
130
|
+
end
|
131
|
+
|
132
|
+
def rel.cls_for_name=(cls_for_name)
|
133
|
+
@cls_for_name = cls_for_name
|
134
|
+
end
|
135
|
+
|
136
|
+
rel.cls_for_name = @cls_for_name
|
137
|
+
instance_variable_set(:@relation, rel)
|
138
|
+
|
139
|
+
#Might need to be in class_eval
|
140
|
+
belongs_to lookup_name.to_s.to_sym, :foreign_key => "#{as_name}"
|
141
|
+
validates as_name.to_s.to_sym, :presence => true
|
142
|
+
|
143
|
+
# Prefill the hashes from the DB - requires an active connection
|
144
|
+
all_vals = cls.all
|
145
|
+
# No need for the class_exec
|
146
|
+
cls.class_variable_set(:@@rcaches, all_vals.inject({}) do |r, obj|
|
147
|
+
r[obj.name] = obj.id
|
148
|
+
r
|
149
|
+
end)
|
150
|
+
cls.class_variable_set(:@@caches, all_vals.inject([]) do |r, obj|
|
151
|
+
r[obj.id] = obj.name
|
152
|
+
r
|
153
|
+
end)
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
157
|
+
|
158
|
+
# extend host class with class methods when we're included
|
159
|
+
def self.included(host_class)
|
160
|
+
host_class.extend(ClassMethods)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
data/rails_lookup.gemspec
CHANGED
@@ -1,172 +1,28 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "rails_lookup/version"
|
2
4
|
|
3
5
|
Gem::Specification.new do |s|
|
4
|
-
s.name
|
5
|
-
s.version
|
6
|
+
s.name = %q{rails_lookup}
|
7
|
+
s.version = RailsLookup::VERSION
|
8
|
+
s.authors = ["Nimrod Priell"]
|
9
|
+
s.email = ["nimrod.priell@gmail.com"]
|
10
|
+
s.homepage = %q{http://github.com/Nimster/RailsLookup/}
|
11
|
+
s.summary = %q{Lookup table macro for ActiveRecords. See more in the README}
|
12
|
+
|
13
|
+
s.rubyforge_project = %q{rails_lookup}
|
6
14
|
|
7
15
|
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
8
|
-
s.authors = [%q{Nimrod Priell}]
|
9
16
|
s.date = %q{2011-08-09}
|
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
|
-
Installation is described down the page.
|
18
|
-
|
19
|
-
Motivation
|
20
|
-
----------
|
21
|
-
|
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
|
23
|
-
ID | car_name | car_type
|
24
|
-
1 | Chevrolet Aveo | Compact
|
25
|
-
2 | Ford Fiesta | Compact
|
26
|
-
3 | BMW Z-5 | Sports
|
27
|
-
|
28
|
-
You want to have two tables:
|
29
|
-
ID | car_name | car_type_id
|
30
|
-
1 | Chevrolet Aveo | 1
|
31
|
-
2 | Ford Fiesta | 1
|
32
|
-
3 | BMW Z-5 | 2
|
33
|
-
|
34
|
-
And
|
35
|
-
|
36
|
-
car_type_id | car_type_name
|
37
|
-
1 | Compact
|
38
|
-
2 | Sports
|
39
|
-
|
40
|
-
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.
|
41
|
-
|
42
|
-
The usual way to do this with ruby on rails is
|
43
|
-
* Generate a CarType model using `rails generate model CarType name:string`
|
44
|
-
* Link between CarType and Car tables using `belongs_to` and `has_many`
|
45
|
-
|
46
|
-
Then to work with this you can transparently read the car type:
|
47
|
-
|
48
|
-
car = Car.all.first
|
49
|
-
car.car_type.name # returns "Compact"
|
50
|
-
|
51
|
-
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.
|
52
|
-
|
53
|
-
You can even make this shorter, by defining a delegate to car_type_name from CarType:
|
54
|
-
|
55
|
-
*in car_type_name.rb*
|
56
|
-
|
57
|
-
delegate :name, :to => :car, :prefix => true
|
58
|
-
|
59
|
-
And now you can access this as
|
60
|
-
|
61
|
-
car.car_type_name
|
62
|
-
|
63
|
-
However, it's less pleasant to insert with this technique:
|
64
|
-
|
65
|
-
car.car_type.car_type_name = "Sports"
|
66
|
-
car.car_type.save!
|
67
|
-
#Now let's see what happened to the OTHER compact car
|
68
|
-
Car.all.second.car_type_name #Oops, returns "Sports"
|
69
|
-
|
70
|
-
Right, what are we doing? We should've used
|
71
|
-
|
72
|
-
car.update_attributes(car_type: CarType.find_or_create_by_name(name: "Sports"))
|
73
|
-
|
74
|
-
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…
|
75
|
-
|
76
|
-
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.
|
77
|
-
|
78
|
-
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:
|
79
|
-
|
80
|
-
car.all.first
|
81
|
-
car.car_type #return "Compact"
|
82
|
-
car.car_type = "Sports" #No effect on car.all.second, just automatically use the second constant
|
83
|
-
car.car_type = "Sedan" #Magically create a new type
|
84
|
-
|
85
|
-
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:
|
86
|
-
|
87
|
-
CarType.where (:id > 3) #Just an example of supposed "arbitrary" SQL involving a real live CarType class
|
88
|
-
|
89
|
-
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?
|
90
|
-
|
91
|
-
I've looked thoroughly for a nice rails solution to this, but after failing to find one, I created my own rails metaprogramming hook.
|
92
|
-
|
93
|
-
Installation
|
94
|
-
------------
|
95
|
-
First, run `gem install rails_lookup` to install the latest version of the gem.
|
96
|
-
|
97
|
-
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):
|
98
|
-
In your ActiveRecord object simply add
|
99
|
-
|
100
|
-
require 'active_record/lookup'
|
101
|
-
class Car < ActiveRecord::Base
|
102
|
-
#...
|
103
|
-
include ActiveRecord::Lookup
|
104
|
-
lookup :car_type
|
105
|
-
#...
|
106
|
-
end
|
107
|
-
|
108
|
-
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
|
109
|
-
|
110
|
-
CarType.id_for "Sports" #Returns 2
|
111
|
-
CarType.name_for 1 #Returns "Compact"
|
112
|
-
|
113
|
-
and you can still hack at the underlying ID for an object, if you need to:
|
114
|
-
|
115
|
-
car = car.all.first
|
116
|
-
car.car_type = "Sports"
|
117
|
-
car.car_type_id #Returns 2
|
118
|
-
car.car_type_id = 1
|
119
|
-
car.car_type #Returns "Compact"
|
120
|
-
|
121
|
-
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
|
122
|
-
|
123
|
-
rails generate migration create_car_type_lookup_for_car
|
124
|
-
|
125
|
-
will automatically create the migration
|
126
|
-
|
127
|
-
class CreateCarTypeLookupForCar < ActiveRecord::Migration
|
128
|
-
def self.up
|
129
|
-
create_table :car_types do |t|
|
130
|
-
t.string :name
|
131
|
-
t.timestamps #Btw you can remove these, I don't much like them in type tables anyway
|
132
|
-
end
|
133
|
-
|
134
|
-
remove_column :cars, :car_type #Let's assume you have one of those now…
|
135
|
-
add_column :cars, :car_type_id, :integer #Maybe put not_null constraints here.
|
136
|
-
end
|
137
|
-
|
138
|
-
def self.down
|
139
|
-
drop_table :car_types
|
140
|
-
add_column :cars, :car_type, :string
|
141
|
-
remove_column :cars, :car_type_id
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
I'll let you work out the details for actually migrating the data yourself.
|
146
|
-
|
147
|
-
[1] http://en.wikipedia.org/wiki/Database_normalization
|
148
|
-
[2] http://en.wikipedia.org/wiki/Column-oriented_DBMS
|
149
|
-
|
150
|
-
I hope this helped you and saved a lot of time and frustration. Follow me on twitter: @nimrodpriell
|
151
|
-
|
152
|
-
}
|
153
|
-
s.email = %q{@nimrodpriell}
|
154
|
-
s.extra_rdoc_files = [%q{README}, %q{lib/active_record/lookup.rb}]
|
155
|
-
s.files = [%q{README}, %q{Rakefile}, %q{lib/active_record/lookup.rb}, %q{rails_lookup.gemspec}, %q{test/20110808002412_create_test_lookup_db.rb}, %q{test/test_lookup.rb}]
|
156
|
-
s.homepage = %q{http://github.com/Nimster/RailsLookup/}
|
157
|
-
s.rdoc_options = [%q{--line-numbers}, %q{--inline-source}, %q{--title}, %q{Rails_lookup}, %q{--main}, %q{README}]
|
158
|
-
s.require_paths = [%q{lib}]
|
159
|
-
s.rubyforge_project = %q{rails_lookup}
|
160
|
-
s.rubygems_version = %q{1.8.7}
|
161
|
-
s.summary = %q{Lookup table macro for ActiveRecords}
|
162
|
-
s.test_files = [%q{test/test_lookup.rb}]
|
17
|
+
s.description = %q{Lookup tables with ruby-on-rails}
|
163
18
|
|
164
|
-
|
165
|
-
|
19
|
+
s.files = `git ls-files`.split("\n")
|
20
|
+
s.test_files = `git ls-files -- {test,spec,features}/*_test.rb`.split("\n")
|
21
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
22
|
+
s.require_paths = ["lib"]
|
23
|
+
s.extra_rdoc_files = [%q{README}, %q{lib/rails_lookup.rb}]
|
24
|
+
s.rdoc_options = [%q{--line-numbers}, %q{--inline-source}, %q{--title}, %q{rails_lookup}, %q{--main}, %q{README}]
|
166
25
|
|
167
|
-
|
168
|
-
|
169
|
-
end
|
170
|
-
else
|
171
|
-
end
|
26
|
+
s.add_dependency 'rails'
|
27
|
+
s.add_development_dependency 'sqlite3'
|
172
28
|
end
|
@@ -1,18 +1,44 @@
|
|
1
|
-
require '
|
1
|
+
require 'active_record'
|
2
|
+
$:.push File.expand_path('lib')
|
3
|
+
#$:.push File.expand_path('../lib', __FILE__)
|
4
|
+
require 'rails_lookup'
|
2
5
|
require 'minitest/autorun'
|
6
|
+
require '20110808002412_create_test_lookup_db'
|
7
|
+
|
8
|
+
# It sucks we have to do that in testing code, but I have to establish a
|
9
|
+
# connection prior to defining any classes that use lookups, and I see no other
|
10
|
+
# way around it other than extracting the test classes and requiring them only
|
11
|
+
# after the connection is established in setup() - which might be a nice
|
12
|
+
# solution but muddles things up.
|
13
|
+
# Establish a connection to the test database:
|
14
|
+
conn = ActiveRecord::Base.establish_connection(
|
15
|
+
adapter: "sqlite3",
|
16
|
+
database: "test/test.sqlite3",
|
17
|
+
pool: 5,
|
18
|
+
timeout: 5000
|
19
|
+
)
|
20
|
+
puts "Established connection!" unless conn.nil?
|
21
|
+
CreateTestLookupDb.migrate(:down) # Connection has to be established for this to work
|
22
|
+
CreateTestLookupDb.migrate(:up)
|
3
23
|
|
4
24
|
class Car < ActiveRecord::Base
|
5
|
-
include
|
25
|
+
include RailsLookup
|
6
26
|
lookup :car_kind, :as => :kind
|
7
27
|
lookup :car_color, :as => :color
|
28
|
+
|
29
|
+
# Example of how to scope for lookups
|
30
|
+
scope :by_color, lambda { |color|
|
31
|
+
joins(:car_color)\
|
32
|
+
.where({:car_colors => { name: color }})
|
33
|
+
}
|
8
34
|
end
|
9
35
|
|
10
36
|
class Plane < ActiveRecord::Base
|
11
|
-
include
|
37
|
+
include RailsLookup
|
12
38
|
lookup :plane_kind, :as => :kind
|
13
39
|
end
|
14
40
|
|
15
|
-
class
|
41
|
+
class LookupTest < MiniTest::Unit::TestCase
|
16
42
|
|
17
43
|
def setup
|
18
44
|
clear_tables
|
@@ -23,6 +49,9 @@ class TestLookup < MiniTest::Unit::TestCase
|
|
23
49
|
end
|
24
50
|
|
25
51
|
def clear_tables
|
52
|
+
# In every test you ever do, these calls will be required for all lookup
|
53
|
+
# tables involved, since they take care of clearing the (static) caches.
|
54
|
+
# Otherwise, you'd have a stale cache in the lookup class.
|
26
55
|
Car.delete_all
|
27
56
|
CarKind.delete_all
|
28
57
|
CarColor.delete_all
|
@@ -129,11 +158,13 @@ class TestLookup < MiniTest::Unit::TestCase
|
|
129
158
|
assert_equal "Yellow", bimba2.color
|
130
159
|
assert_equal "Compact", bimba2.kind
|
131
160
|
cars_before_creation = Car.count
|
132
|
-
susita = Car.find_or_create_by_name_and_kind_and_color
|
161
|
+
susita = Car.find_or_create_by_name_and_kind_and_color(\
|
162
|
+
"Susita", "Compact", "Gray")
|
133
163
|
assert_equal "Gray", susita.color
|
134
164
|
assert_equal "Compact", susita.kind
|
135
165
|
assert_equal cars_before_creation + 1, Car.count
|
136
|
-
bimba2 = Car.find_or_create_by_name_and_kind_and_color
|
166
|
+
bimba2 = Car.find_or_create_by_name_and_kind_and_color(\
|
167
|
+
"Bimba", "Compact", "Yellow")
|
137
168
|
assert_equal "Yellow", bimba2.color
|
138
169
|
assert_equal "Compact", bimba2.kind
|
139
170
|
assert_equal "Bimba", bimba2.name
|
@@ -179,7 +210,7 @@ class TestLookup < MiniTest::Unit::TestCase
|
|
179
210
|
#We get a new CarKind class - but this one will be preloaded
|
180
211
|
tmp = CarKind
|
181
212
|
Class.new(ActiveRecord::Base) do
|
182
|
-
include
|
213
|
+
include RailsLookup
|
183
214
|
lookup :car_kind
|
184
215
|
end
|
185
216
|
refute_nil CarKind.id_for("Compact")
|
@@ -208,40 +239,13 @@ class TestLookup < MiniTest::Unit::TestCase
|
|
208
239
|
# yellow = CarColor.find_by_name "Yellow"
|
209
240
|
# assert_equal 2, yellow.cars.size
|
210
241
|
end
|
242
|
+
|
243
|
+
# TODO: Test a scope
|
244
|
+
def test_scopes
|
245
|
+
bimba = Car.create! :name => "Bimba", :kind => "Compact", :color => "Yellow"
|
246
|
+
ferrari = Car.create! :kind => "Sports", :color => "Red"
|
247
|
+
assert_equal 1, Car.by_color("Yellow").count
|
248
|
+
assert_equal 1, Car.by_color("Red").count
|
249
|
+
end
|
211
250
|
end
|
212
251
|
|
213
|
-
=begin
|
214
|
-
a = TestLookup.new
|
215
|
-
puts "Cache hits: "
|
216
|
-
puts(TestLookupKind.id_for "simian")
|
217
|
-
puts "Setting test_lookup_kind: "
|
218
|
-
a.test_lookup_kind="simian"
|
219
|
-
puts "Reading test_lookup_kind: "
|
220
|
-
puts a.test_lookup_kind
|
221
|
-
puts a.test_lookup_kind_id
|
222
|
-
|
223
|
-
puts "Hacking the ID: "
|
224
|
-
a.test_lookup_kind_id = 1
|
225
|
-
puts a.test_lookup_kind
|
226
|
-
|
227
|
-
puts "Setting another_lookup_kind: "
|
228
|
-
a.another_lookup_kind="simian"
|
229
|
-
puts "Reading another_lookup_kind: "
|
230
|
-
puts a.another_lookup_kind
|
231
|
-
puts a.another_lookup_kind_id
|
232
|
-
|
233
|
-
b = TestLookupKind.new
|
234
|
-
puts "HERE"
|
235
|
-
p TestLookupKind.all
|
236
|
-
|
237
|
-
puts "Testing different class"
|
238
|
-
b = OneMore.new
|
239
|
-
b.taverna_kind = "williwok"
|
240
|
-
puts b.taverna_kind
|
241
|
-
puts b.taverna_kind_id
|
242
|
-
|
243
|
-
puts "Testing original class: "
|
244
|
-
puts a.test_lookup_kind
|
245
|
-
puts a.another_lookup_kind
|
246
|
-
|
247
|
-
=end
|
metadata
CHANGED
@@ -1,115 +1,86 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails_lookup
|
3
|
-
version: !ruby/object:Gem::Version
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.4
|
4
5
|
prerelease:
|
5
|
-
version: 0.0.3
|
6
6
|
platform: ruby
|
7
|
-
authors:
|
7
|
+
authors:
|
8
8
|
- Nimrod Priell
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
* Generate a CarType model using `rails generate model CarType name:string`\n\
|
40
|
-
* Link between CarType and Car tables using `belongs_to` and `has_many`\n\n\
|
41
|
-
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\
|
42
|
-
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\
|
43
|
-
You can even make this shorter, by defining a delegate to car_type_name from CarType:\n\n\
|
44
|
-
*in car_type_name.rb*\n \n delegate :name, :to => :car, :prefix => true\n\n\
|
45
|
-
And now you can access this as \n\n car.car_type_name\n\n\
|
46
|
-
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\
|
47
|
-
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\
|
48
|
-
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\
|
49
|
-
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\
|
50
|
-
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\
|
51
|
-
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\
|
52
|
-
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\
|
53
|
-
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\
|
54
|
-
Installation\n\
|
55
|
-
------------\n\
|
56
|
-
First, run `gem install rails_lookup` to install the latest version of the gem.\n\n\
|
57
|
-
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\
|
58
|
-
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\
|
59
|
-
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\
|
60
|
-
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\
|
61
|
-
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\
|
62
|
-
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\
|
63
|
-
I'll let you work out the details for actually migrating the data yourself. \n\n\
|
64
|
-
[1] http://en.wikipedia.org/wiki/Database_normalization\n\
|
65
|
-
[2] http://en.wikipedia.org/wiki/Column-oriented_DBMS\n\n\
|
66
|
-
I hope this helped you and saved a lot of time and frustration. Follow me on twitter: @nimrodpriell\n\n"
|
67
|
-
email: "@nimrodpriell"
|
12
|
+
date: 2011-08-09 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rails
|
16
|
+
requirement: &70313073195380 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70313073195380
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: sqlite3
|
27
|
+
requirement: &70313073194820 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70313073194820
|
36
|
+
description: Lookup tables with ruby-on-rails
|
37
|
+
email:
|
38
|
+
- nimrod.priell@gmail.com
|
68
39
|
executables: []
|
69
|
-
|
70
40
|
extensions: []
|
71
|
-
|
72
|
-
extra_rdoc_files:
|
41
|
+
extra_rdoc_files:
|
73
42
|
- README
|
74
|
-
- lib/
|
75
|
-
files:
|
43
|
+
- lib/rails_lookup.rb
|
44
|
+
files:
|
45
|
+
- .gitignore
|
46
|
+
- Gemfile
|
76
47
|
- README
|
77
48
|
- Rakefile
|
78
|
-
- lib/
|
49
|
+
- lib/rails_lookup.rb
|
50
|
+
- lib/rails_lookup/version.rb
|
51
|
+
- rails_lookup-0.0.3.gem
|
79
52
|
- rails_lookup.gemspec
|
80
53
|
- test/20110808002412_create_test_lookup_db.rb
|
81
|
-
- test/
|
54
|
+
- test/lookup_test.rb
|
82
55
|
homepage: http://github.com/Nimster/RailsLookup/
|
83
56
|
licenses: []
|
84
|
-
|
85
57
|
post_install_message:
|
86
|
-
rdoc_options:
|
58
|
+
rdoc_options:
|
87
59
|
- --line-numbers
|
88
60
|
- --inline-source
|
89
61
|
- --title
|
90
|
-
-
|
62
|
+
- rails_lookup
|
91
63
|
- --main
|
92
64
|
- README
|
93
|
-
require_paths:
|
65
|
+
require_paths:
|
94
66
|
- lib
|
95
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
96
68
|
none: false
|
97
|
-
requirements:
|
98
|
-
- -
|
99
|
-
- !ruby/object:Gem::Version
|
100
|
-
version:
|
101
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ! '>='
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
74
|
none: false
|
103
|
-
requirements:
|
104
|
-
- -
|
105
|
-
- !ruby/object:Gem::Version
|
106
|
-
version:
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '1.2'
|
107
79
|
requirements: []
|
108
|
-
|
109
80
|
rubyforge_project: rails_lookup
|
110
|
-
rubygems_version: 1.8.
|
81
|
+
rubygems_version: 1.8.15
|
111
82
|
signing_key:
|
112
83
|
specification_version: 3
|
113
|
-
summary: Lookup table macro for ActiveRecords
|
114
|
-
test_files:
|
115
|
-
- test/
|
84
|
+
summary: Lookup table macro for ActiveRecords. See more in the README
|
85
|
+
test_files:
|
86
|
+
- test/lookup_test.rb
|
data/lib/active_record/lookup.rb
DELETED
@@ -1,153 +0,0 @@
|
|
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 so they can be called as "macros"
|
10
|
-
#like belongs_to
|
11
|
-
module ClassMethods
|
12
|
-
#We define only this "macro". In your ActiveRecord entity (say, Car), use
|
13
|
-
# lookup :car_type
|
14
|
-
# which will create Car#car_type, Car#car_type=, Car#car_type_id,
|
15
|
-
# Car#car_type_id= and CarType, an ActiveRecord with a name (String)
|
16
|
-
# attribute, that has_many :cars.
|
17
|
-
#
|
18
|
-
# You can also use
|
19
|
-
# lookup :car_type, :as => :type
|
20
|
-
# which will do the same thing but create Car#type, Car#type=, Car#type_id
|
21
|
-
# and Car#type_id= instead.
|
22
|
-
def lookup(lookup_name, opts = {})
|
23
|
-
as_name = opts[:as] || lookup_name
|
24
|
-
|
25
|
-
#Define the setter for the attribute by textual value
|
26
|
-
mycls = self #Class I'm defined in
|
27
|
-
#Ignore anonymous classes:
|
28
|
-
mycls_sym = mycls.name.tableize.to_sym unless mycls.to_s =~ /#<.*>/
|
29
|
-
lookup_cls_name = lookup_name.to_s.camelize
|
30
|
-
begin
|
31
|
-
#If it exists, just tack on the extra has_many
|
32
|
-
cls = Object.const_get lookup_cls_name
|
33
|
-
cls.has_many mycls_sym unless mycls_sym.blank? #for annon classes
|
34
|
-
rescue NameError
|
35
|
-
#Otherwise, actually define it
|
36
|
-
cls = Class.new(ActiveRecord::Base) do
|
37
|
-
has_many mycls_sym unless mycls_sym.blank? #For anon classes
|
38
|
-
validates_uniqueness_of :name
|
39
|
-
validates :name, :presence => true
|
40
|
-
|
41
|
-
def self.id_for(name, id = nil)
|
42
|
-
class_variable_get(:@@rcaches)[name] ||= id
|
43
|
-
end
|
44
|
-
|
45
|
-
def self.gen_id_for(val)
|
46
|
-
id = id_for val
|
47
|
-
if id.nil?
|
48
|
-
#Define this new possible value
|
49
|
-
new_db_obj = find_or_create_by_name val #You could just override this...
|
50
|
-
id_for val, new_db_obj.id
|
51
|
-
name_for new_db_obj.id, val
|
52
|
-
id = new_db_obj.id
|
53
|
-
end
|
54
|
-
id
|
55
|
-
end
|
56
|
-
|
57
|
-
def self.name_for(id, name = nil)
|
58
|
-
class_variable_get(:@@caches)[id] ||= name
|
59
|
-
end
|
60
|
-
|
61
|
-
#This is especially useful for tests - you want deletion to also clear
|
62
|
-
#the caches otherwise odd stuff might happen
|
63
|
-
def self.delete_all(*args)
|
64
|
-
class_variable_set :@@caches, []
|
65
|
-
class_variable_set :@@rcaches, {}
|
66
|
-
super *args
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
Object.const_set lookup_cls_name, cls #Define it as a global class
|
71
|
-
end
|
72
|
-
|
73
|
-
|
74
|
-
define_method("#{as_name.to_s}_id=") do |id|
|
75
|
-
write_attribute "#{as_name.to_s}".to_sym, id
|
76
|
-
end
|
77
|
-
|
78
|
-
define_method("#{as_name.to_s}=") do |val|
|
79
|
-
id = cls.gen_id_for val #TODO: Just call #{as_name.to_s}_id=
|
80
|
-
write_attribute "#{as_name.to_s}".to_sym, id
|
81
|
-
end
|
82
|
-
|
83
|
-
define_method("#{as_name.to_s}_id") do
|
84
|
-
read_attribute "#{as_name.to_s}".to_sym
|
85
|
-
end
|
86
|
-
|
87
|
-
#Define the getter
|
88
|
-
define_method("#{as_name.to_s}") do
|
89
|
-
id = read_attribute "#{as_name.to_s}".to_sym
|
90
|
-
if not id.nil?
|
91
|
-
value = cls.name_for id
|
92
|
-
if value.nil?
|
93
|
-
lookup_obj = cls.find_by_id id
|
94
|
-
if not lookup_obj.nil?
|
95
|
-
#TODO: Extract to a method
|
96
|
-
cls.name_for id, lookup_obj.name
|
97
|
-
cls.id_for lookup_obj.name, id
|
98
|
-
end
|
99
|
-
end
|
100
|
-
end
|
101
|
-
value
|
102
|
-
end
|
103
|
-
|
104
|
-
|
105
|
-
#Rails ActiveRecord support:
|
106
|
-
@cls_for_name = {} if @cls_for_name.nil?
|
107
|
-
@cls_for_name[as_name.to_s.to_sym] = cls
|
108
|
-
|
109
|
-
#This makes find_and_create_by, find_all and where methods all work as
|
110
|
-
#they should. This will not work if you use reset_column_information,
|
111
|
-
#like if you use find_by_session_id in SessionStore. You must redefine
|
112
|
-
#it so it re-sets rel to this singleton object
|
113
|
-
rel = ActiveRecord::Relation.new(self, arel_table)
|
114
|
-
def rel.where(opts, *rest)
|
115
|
-
if opts.is_a? Hash
|
116
|
-
mapped = opts.map { |k,v| @cls_for_name.has_key?(k.to_sym) ? [k, @cls_for_name[k.to_sym].id_for(v)] : [k, v] }
|
117
|
-
opts = Hash[mapped]
|
118
|
-
end
|
119
|
-
super(opts, *rest)
|
120
|
-
end
|
121
|
-
|
122
|
-
def rel.cls_for_name=(cls_for_name)
|
123
|
-
@cls_for_name = cls_for_name
|
124
|
-
end
|
125
|
-
|
126
|
-
rel.cls_for_name = @cls_for_name
|
127
|
-
instance_variable_set(:@relation, rel)
|
128
|
-
|
129
|
-
#Might need to be in class_eval
|
130
|
-
belongs_to lookup_name.to_s.to_sym, :foreign_key => "#{as_name}_id"
|
131
|
-
validates as_name.to_s.to_sym, :presence => true
|
132
|
-
|
133
|
-
#Prefill the hashes from the DB:
|
134
|
-
all_vals = cls.all
|
135
|
-
#No need for the class_exec
|
136
|
-
cls.class_variable_set(:@@rcaches, all_vals.inject({}) do |r, obj|
|
137
|
-
r[obj.name] = obj.id
|
138
|
-
r
|
139
|
-
end)
|
140
|
-
cls.class_variable_set(:@@caches, all_vals.inject([]) do |r, obj|
|
141
|
-
r[obj.id] = obj.name
|
142
|
-
r
|
143
|
-
end)
|
144
|
-
end
|
145
|
-
|
146
|
-
end
|
147
|
-
|
148
|
-
# extend host class with class methods when we're included
|
149
|
-
def self.included(host_class)
|
150
|
-
host_class.extend(ClassMethods)
|
151
|
-
end
|
152
|
-
end
|
153
|
-
end
|