sequel 1.3 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +127 -0
- data/COPYING +1 -0
- data/README +5 -4
- data/Rakefile +78 -25
- data/lib/sequel.rb +1 -2
- data/lib/sequel_model.rb +324 -0
- data/lib/sequel_model/associations.rb +351 -0
- data/lib/sequel_model/base.rb +120 -0
- data/lib/sequel_model/caching.rb +42 -0
- data/lib/sequel_model/eager_loading.rb +169 -0
- data/lib/sequel_model/hooks.rb +55 -0
- data/lib/sequel_model/plugins.rb +47 -0
- data/lib/sequel_model/pretty_table.rb +73 -0
- data/lib/sequel_model/record.rb +336 -0
- data/lib/sequel_model/schema.rb +48 -0
- data/lib/sequel_model/validations.rb +15 -0
- data/spec/associations_spec.rb +712 -0
- data/spec/base_spec.rb +239 -0
- data/spec/caching_spec.rb +150 -0
- data/spec/deprecated_relations_spec.rb +153 -0
- data/spec/eager_loading_spec.rb +260 -0
- data/spec/hooks_spec.rb +269 -0
- data/spec/model_spec.rb +543 -0
- data/spec/plugins_spec.rb +74 -0
- data/spec/rcov.opts +4 -0
- data/spec/record_spec.rb +593 -0
- data/spec/schema_spec.rb +69 -0
- data/spec/spec.opts +5 -0
- data/spec/spec_helper.rb +43 -0
- data/spec/validations_spec.rb +246 -0
- metadata +90 -56
@@ -0,0 +1,42 @@
|
|
1
|
+
module Sequel
|
2
|
+
class Model
|
3
|
+
def self.set_cache(store, opts = {})
|
4
|
+
@cache_store = store
|
5
|
+
if (ttl = opts[:ttl])
|
6
|
+
set_cache_ttl(ttl)
|
7
|
+
end
|
8
|
+
|
9
|
+
meta_def(:[]) do |*args|
|
10
|
+
if (args.size == 1) && (Hash === (h = args.first))
|
11
|
+
return dataset[h]
|
12
|
+
end
|
13
|
+
|
14
|
+
unless obj = @cache_store.get(cache_key_from_values(args))
|
15
|
+
obj = dataset[primary_key_hash((args.size == 1) ? args.first : args)]
|
16
|
+
@cache_store.set(cache_key_from_values(args), obj, cache_ttl)
|
17
|
+
end
|
18
|
+
obj
|
19
|
+
end
|
20
|
+
|
21
|
+
class_def(:set) {|v| store.delete(cache_key); super}
|
22
|
+
class_def(:save) {store.delete(cache_key); super}
|
23
|
+
class_def(:delete) {store.delete(cache_key); super}
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.set_cache_ttl(ttl)
|
27
|
+
@cache_ttl = ttl
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.cache_store
|
31
|
+
@cache_store
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.cache_ttl
|
35
|
+
@cache_ttl ||= 3600
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.cache_key_from_values(values)
|
39
|
+
"#{self}:#{values.join(',')}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
# Eager loading makes it so that you can load all associated records for a
|
2
|
+
# set of objects in a single query, instead of a separate query for each object.
|
3
|
+
#
|
4
|
+
# The basic idea for how it works is that the dataset is first loaded normally.
|
5
|
+
# Then it goes through all associations that have been specified via .eager.
|
6
|
+
# It loads each of those associations separately, then associates them back
|
7
|
+
# to the original dataset via primary/foreign keys. Due to the necessity of
|
8
|
+
# all objects being present, you need to use .all to use eager loading, as it
|
9
|
+
# can't work with .each.
|
10
|
+
#
|
11
|
+
# This implementation avoids the complexity of extracting an object graph out
|
12
|
+
# of a single dataset, by building the object graph out of multiple datasets,
|
13
|
+
# one for each association. By using a separate dataset for each association,
|
14
|
+
# it avoids problems such as aliasing conflicts and creating cartesian product
|
15
|
+
# result sets if multiple *_to_many eager associations are requested.
|
16
|
+
#
|
17
|
+
# One limitation of using this method is that you cannot filter the dataset
|
18
|
+
# based on values of columns in an associated table, since the associations are loaded
|
19
|
+
# in separate queries. To do that you need to load all associations in the
|
20
|
+
# same query, and extract an object graph from the results of that query.
|
21
|
+
#
|
22
|
+
# You can cascade the eager loading (loading associations' associations)
|
23
|
+
# with no limit to the depth of the cascades. You do this by passing a hash to .eager
|
24
|
+
# with the keys being associations of the current model and values being
|
25
|
+
# associations of the model associated with the current model via the key.
|
26
|
+
#
|
27
|
+
# The associations' order, if defined, is respected. You cannot eagerly load
|
28
|
+
# an association with a block argument, as the block argument is evaluated in
|
29
|
+
# terms of a specific instance of the model, and no specific instance exists
|
30
|
+
# when eagerly loading.
|
31
|
+
module Sequel::Model::Associations::EagerLoading
|
32
|
+
# Add associations to the list of associations to eagerly load.
|
33
|
+
# Associations can be a symbol or a hash with symbol keys (for cascaded
|
34
|
+
# eager loading). Examples:
|
35
|
+
#
|
36
|
+
# Album.eager(:artist).all
|
37
|
+
# Album.eager(:artist, :genre).all
|
38
|
+
# Album.eager(:artist).eager(:genre).all
|
39
|
+
# Artist.eager(:albums=>:tracks).all
|
40
|
+
# Artist.eager(:albums=>{:tracks=>:genre}).all
|
41
|
+
def eager(*associations)
|
42
|
+
raise(ArgumentError, 'No model for this dataset') unless @opts[:models] && model = @opts[:models][nil]
|
43
|
+
opt = @opts[:eager]
|
44
|
+
opt = opt ? opt.dup : {}
|
45
|
+
check = Proc.new do |a|
|
46
|
+
raise(ArgumentError, 'Invalid association') unless reflection = model.association_reflection(a)
|
47
|
+
raise(ArgumentError, 'Cannot eagerly load associations with block arguments') if reflection[:block]
|
48
|
+
end
|
49
|
+
associations.flatten.each do |association|
|
50
|
+
case association
|
51
|
+
when Symbol
|
52
|
+
check.call(association)
|
53
|
+
opt[association] = nil
|
54
|
+
when Hash
|
55
|
+
association.keys.each{|assoc| check.call(assoc)}
|
56
|
+
opt.merge!(association)
|
57
|
+
else raise(ArgumentError, 'Associations must be in the form of a symbol or hash')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
ds = clone(:eager=>opt)
|
61
|
+
ds.add_callback(:post_load, :eager_load) unless @opts[:eager]
|
62
|
+
ds
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
# Eagerly load all specified associations
|
67
|
+
def eager_load(a)
|
68
|
+
return if a.empty?
|
69
|
+
# Current model class
|
70
|
+
model = @opts[:models][nil]
|
71
|
+
# All associations to eager load
|
72
|
+
eager_assoc = @opts[:eager]
|
73
|
+
# Key is foreign/primary key name symbol
|
74
|
+
# Value is hash with keys being foreign/primary key values (generally integers)
|
75
|
+
# and values being an array of current model objects with that
|
76
|
+
# specific foreign/primary key
|
77
|
+
key_hash = {}
|
78
|
+
# array of attribute_values keys to monitor
|
79
|
+
keys = []
|
80
|
+
# Reflections for all associations to eager load
|
81
|
+
reflections = eager_assoc.keys.collect{|assoc| model.association_reflection(assoc)}
|
82
|
+
|
83
|
+
# Populate keys to monitor
|
84
|
+
reflections.each do |reflection|
|
85
|
+
key = reflection[:type] == :many_to_one ? reflection[:key] : model.primary_key
|
86
|
+
next if key_hash[key]
|
87
|
+
key_hash[key] = {}
|
88
|
+
keys << key
|
89
|
+
end
|
90
|
+
|
91
|
+
# Associate each object with every key being monitored
|
92
|
+
a.each do |r|
|
93
|
+
keys.each do |key|
|
94
|
+
((key_hash[key][r[key]] ||= []) << r) if r[key]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Iterate through eager associations and assign instance variables
|
99
|
+
# for the association for all model objects
|
100
|
+
reflections.each do |reflection|
|
101
|
+
assoc_class = model.send(:associated_class, reflection)
|
102
|
+
assoc_name = reflection[:name]
|
103
|
+
# Proc for setting cascaded eager loading
|
104
|
+
cascade = Proc.new do |d|
|
105
|
+
if c = eager_assoc[assoc_name]
|
106
|
+
d = d.eager(c)
|
107
|
+
end
|
108
|
+
if c = reflection[:eager]
|
109
|
+
d = d.eager(c)
|
110
|
+
end
|
111
|
+
d
|
112
|
+
end
|
113
|
+
case rtype = reflection[:type]
|
114
|
+
when :many_to_one
|
115
|
+
key = reflection[:key]
|
116
|
+
h = key_hash[key]
|
117
|
+
keys = h.keys
|
118
|
+
# No records have the foreign key set for this association, so skip it
|
119
|
+
next unless keys.length > 0
|
120
|
+
ds = assoc_class.filter(assoc_class.primary_key=>keys)
|
121
|
+
ds = cascade.call(ds)
|
122
|
+
ds.all do |assoc_object|
|
123
|
+
h[assoc_object.pk].each do |object|
|
124
|
+
object.instance_variable_set(:"@#{assoc_name}", assoc_object)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
when :one_to_many, :many_to_many
|
128
|
+
if rtype == :one_to_many
|
129
|
+
fkey = key = reflection[:key]
|
130
|
+
h = key_hash[model.primary_key]
|
131
|
+
reciprocal = model.send(:reciprocal_association, reflection)
|
132
|
+
ds = assoc_class.filter(key=>h.keys)
|
133
|
+
else
|
134
|
+
assoc_table = assoc_class.table_name
|
135
|
+
left = reflection[:left_key]
|
136
|
+
right = reflection[:right_key]
|
137
|
+
right_pk = (reflection[:right_primary_key] || :"#{assoc_table}__#{assoc_class.primary_key}")
|
138
|
+
join_table = reflection[:join_table]
|
139
|
+
fkey = (opts[:left_key_alias] ||= :"x_foreign_key_x")
|
140
|
+
table_selection = (opts[:select] ||= assoc_table.all)
|
141
|
+
key_selection = (opts[:left_key_select] ||= :"#{join_table}__#{left}___#{fkey}")
|
142
|
+
h = key_hash[model.primary_key]
|
143
|
+
ds = assoc_class.select(table_selection, key_selection).inner_join(join_table, right=>right_pk, left=>h.keys)
|
144
|
+
end
|
145
|
+
if order = reflection[:order]
|
146
|
+
ds = ds.order(order)
|
147
|
+
end
|
148
|
+
ds = cascade.call(ds)
|
149
|
+
ivar = :"@#{assoc_name}"
|
150
|
+
h.values.each do |object_array|
|
151
|
+
object_array.each do |object|
|
152
|
+
object.instance_variable_set(ivar, [])
|
153
|
+
end
|
154
|
+
end
|
155
|
+
ds.all do |assoc_object|
|
156
|
+
fk = if rtype == :many_to_many
|
157
|
+
assoc_object.values.delete(fkey)
|
158
|
+
else
|
159
|
+
assoc_object[fkey]
|
160
|
+
end
|
161
|
+
h[fk].each do |object|
|
162
|
+
object.instance_variable_get(ivar) << assoc_object
|
163
|
+
assoc_object.instance_variable_set(reciprocal, object) if reciprocal
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Sequel
|
2
|
+
class Model
|
3
|
+
HOOKS = [
|
4
|
+
:after_initialize,
|
5
|
+
:before_create,
|
6
|
+
:after_create,
|
7
|
+
:before_update,
|
8
|
+
:after_update,
|
9
|
+
:before_save,
|
10
|
+
:after_save,
|
11
|
+
:before_destroy,
|
12
|
+
:after_destroy
|
13
|
+
]
|
14
|
+
|
15
|
+
# Some fancy code generation here in order to define the hook class methods...
|
16
|
+
HOOK_METHOD_STR = %Q{
|
17
|
+
def self.%s(method = nil, &block)
|
18
|
+
unless block
|
19
|
+
(raise SequelError, 'No hook method specified') unless method
|
20
|
+
block = proc {send method}
|
21
|
+
end
|
22
|
+
add_hook(%s, &block)
|
23
|
+
end
|
24
|
+
}
|
25
|
+
|
26
|
+
def self.def_hook_method(m) #:nodoc:
|
27
|
+
instance_eval(HOOK_METHOD_STR % [m.to_s, m.inspect])
|
28
|
+
end
|
29
|
+
|
30
|
+
HOOKS.each {|h| define_method(h) {}}
|
31
|
+
HOOKS.each {|h| def_hook_method(h)}
|
32
|
+
|
33
|
+
# Returns the hooks hash for the model class.
|
34
|
+
def self.hooks #:nodoc:
|
35
|
+
@hooks ||= Hash.new {|h, k| h[k] = []}
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.add_hook(hook, &block) #:nodoc:
|
39
|
+
chain = hooks[hook]
|
40
|
+
chain << block
|
41
|
+
define_method(hook) do
|
42
|
+
return false if super == false
|
43
|
+
chain.each {|h| break false if instance_eval(&h) == false}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns true if the model class or any of its ancestors have defined
|
48
|
+
# hooks for the given hook key. Notice that this method cannot detect
|
49
|
+
# hooks defined using overridden methods.
|
50
|
+
def self.has_hooks?(key)
|
51
|
+
has = hooks[key] && !hooks[key].empty?
|
52
|
+
has || ((self != Model) && superclass.has_hooks?(key))
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Sequel
|
2
|
+
module Plugins; end
|
3
|
+
|
4
|
+
class Model
|
5
|
+
class << self
|
6
|
+
# Loads a plugin for use with the model class, passing optional arguments
|
7
|
+
# to the plugin.
|
8
|
+
def is(plugin, *args)
|
9
|
+
m = plugin_module(plugin)
|
10
|
+
if m.respond_to?(:apply)
|
11
|
+
m.apply(self, *args)
|
12
|
+
end
|
13
|
+
if m.const_defined?("InstanceMethods")
|
14
|
+
class_def(:"#{plugin}_opts") {args.first}
|
15
|
+
include(m::InstanceMethods)
|
16
|
+
end
|
17
|
+
if m.const_defined?("ClassMethods")
|
18
|
+
meta_def(:"#{plugin}_opts") {args.first}
|
19
|
+
metaclass.send(:include, m::ClassMethods)
|
20
|
+
end
|
21
|
+
if m.const_defined?("DatasetMethods")
|
22
|
+
unless @dataset
|
23
|
+
raise Sequel::Error, "Plugin cannot be applied because the model class has no dataset"
|
24
|
+
end
|
25
|
+
dataset.meta_def(:"#{plugin}_opts") {args.first}
|
26
|
+
dataset.metaclass.send(:include, m::DatasetMethods)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
alias_method :is_a, :is
|
30
|
+
|
31
|
+
# Returns the module for the specified plugin. If the module is not
|
32
|
+
# defined, the corresponding plugin gem is automatically loaded.
|
33
|
+
def plugin_module(plugin)
|
34
|
+
module_name = plugin.to_s.gsub(/(^|_)(.)/) {$2.upcase}
|
35
|
+
if not Sequel::Plugins.const_defined?(module_name)
|
36
|
+
require plugin_gem(plugin)
|
37
|
+
end
|
38
|
+
Sequel::Plugins.const_get(module_name)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns the gem name for the given plugin.
|
42
|
+
def plugin_gem(plugin)
|
43
|
+
"sequel_#{plugin}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Sequel
|
2
|
+
# Prints nice-looking plain-text tables
|
3
|
+
# +--+-------+
|
4
|
+
# |id|name |
|
5
|
+
# |--+-------|
|
6
|
+
# |1 |fasdfas|
|
7
|
+
# |2 |test |
|
8
|
+
# +--+-------+
|
9
|
+
module PrettyTable
|
10
|
+
def self.records_columns(records)
|
11
|
+
columns = []
|
12
|
+
records.each do |r|
|
13
|
+
if Array === r && (k = r.keys)
|
14
|
+
return k
|
15
|
+
elsif Hash === r
|
16
|
+
r.keys.each {|k| columns << k unless columns.include?(k)}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
columns
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.column_sizes(records, columns)
|
23
|
+
sizes = Hash.new {0}
|
24
|
+
columns.each do |c|
|
25
|
+
s = c.to_s.size
|
26
|
+
sizes[c.to_sym] = s if s > sizes[c.to_sym]
|
27
|
+
end
|
28
|
+
records.each do |r|
|
29
|
+
columns.each do |c|
|
30
|
+
s = r[c].to_s.size
|
31
|
+
sizes[c.to_sym] = s if s > sizes[c.to_sym]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
sizes
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.separator_line(columns, sizes)
|
38
|
+
l = ''
|
39
|
+
'+' + columns.map {|c| '-' * sizes[c]}.join('+') + '+'
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.format_cell(size, v)
|
43
|
+
case v
|
44
|
+
when Bignum, Fixnum
|
45
|
+
"%#{size}d" % v
|
46
|
+
when Float
|
47
|
+
"%#{size}g" % v
|
48
|
+
else
|
49
|
+
"%-#{size}s" % v.to_s
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.data_line(columns, sizes, record)
|
54
|
+
'|' + columns.map {|c| format_cell(sizes[c], record[c])}.join('|') + '|'
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.header_line(columns, sizes)
|
58
|
+
'|' + columns.map {|c| "%-#{sizes[c]}s" % c.to_s}.join('|') + '|'
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.print(records, columns = nil) # records is an array of hashes
|
62
|
+
columns ||= records_columns(records)
|
63
|
+
sizes = column_sizes(records, columns)
|
64
|
+
|
65
|
+
puts separator_line(columns, sizes)
|
66
|
+
puts header_line(columns, sizes)
|
67
|
+
puts separator_line(columns, sizes)
|
68
|
+
records.each {|r| puts data_line(columns, sizes, r)}
|
69
|
+
puts separator_line(columns, sizes)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
@@ -0,0 +1,336 @@
|
|
1
|
+
module Sequel
|
2
|
+
class Model
|
3
|
+
attr_reader :values
|
4
|
+
attr_reader :changed_columns
|
5
|
+
|
6
|
+
# Returns value of attribute.
|
7
|
+
def [](column)
|
8
|
+
@values[column]
|
9
|
+
end
|
10
|
+
# Sets value of attribute and marks the column as changed.
|
11
|
+
def []=(column, value)
|
12
|
+
# If it is new, it doesn't have a value yet, so we should
|
13
|
+
# definitely set the new value.
|
14
|
+
# If the column isn't in @values, we can't assume it is
|
15
|
+
# NULL in the database, so assume it has changed.
|
16
|
+
if new? || !@values.include?(column) || value != @values[column]
|
17
|
+
@changed_columns << column unless @changed_columns.include?(column)
|
18
|
+
@values[column] = value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Enumerates through all attributes.
|
23
|
+
#
|
24
|
+
# === Example:
|
25
|
+
# Ticket.find(7).each { |k, v| puts "#{k} => #{v}" }
|
26
|
+
def each(&block)
|
27
|
+
@values.each(&block)
|
28
|
+
end
|
29
|
+
# Returns attribute names.
|
30
|
+
def keys
|
31
|
+
@values.keys
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns value for <tt>:id</tt> attribute.
|
35
|
+
def id
|
36
|
+
@values[:id]
|
37
|
+
end
|
38
|
+
|
39
|
+
# Compares model instances by values.
|
40
|
+
def ==(obj)
|
41
|
+
(obj.class == model) && (obj.values == @values)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Compares model instances by pkey.
|
45
|
+
def ===(obj)
|
46
|
+
(obj.class == model) && (obj.pk == pk)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns key for primary key.
|
50
|
+
def self.primary_key
|
51
|
+
:id
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns primary key attribute hash.
|
55
|
+
def self.primary_key_hash(value)
|
56
|
+
{:id => value}
|
57
|
+
end
|
58
|
+
|
59
|
+
# Sets primary key, regular and composite are possible.
|
60
|
+
#
|
61
|
+
# == Example:
|
62
|
+
# class Tagging < Sequel::Model
|
63
|
+
# # composite key
|
64
|
+
# set_primary_key :taggable_id, :tag_id
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# class Person < Sequel::Model
|
68
|
+
# # regular key
|
69
|
+
# set_primary_key :person_id
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# <i>You can even set it to nil!</i>
|
73
|
+
def self.set_primary_key(*key)
|
74
|
+
# if k is nil, we go to no_primary_key
|
75
|
+
if key.empty? || (key.size == 1 && key.first == nil)
|
76
|
+
return no_primary_key
|
77
|
+
end
|
78
|
+
|
79
|
+
# backwards compat
|
80
|
+
key = (key.length == 1) ? key[0] : key.flatten
|
81
|
+
|
82
|
+
# redefine primary_key
|
83
|
+
meta_def(:primary_key) {key}
|
84
|
+
|
85
|
+
unless key.is_a? Array # regular primary key
|
86
|
+
class_def(:this) do
|
87
|
+
@this ||= dataset.filter(key => @values[key]).limit(1).naked
|
88
|
+
end
|
89
|
+
class_def(:pk) do
|
90
|
+
@pk ||= @values[key]
|
91
|
+
end
|
92
|
+
class_def(:pk_hash) do
|
93
|
+
@pk ||= {key => @values[key]}
|
94
|
+
end
|
95
|
+
class_def(:cache_key) do
|
96
|
+
pk = @values[key] || (raise Error, 'no primary key for this record')
|
97
|
+
@cache_key ||= "#{self.class}:#{pk}"
|
98
|
+
end
|
99
|
+
meta_def(:primary_key_hash) do |v|
|
100
|
+
{key => v}
|
101
|
+
end
|
102
|
+
else # composite key
|
103
|
+
exp_list = key.map {|k| "#{k.inspect} => @values[#{k.inspect}]"}
|
104
|
+
block = eval("proc {@this ||= self.class.dataset.filter(#{exp_list.join(',')}).limit(1).naked}")
|
105
|
+
class_def(:this, &block)
|
106
|
+
|
107
|
+
exp_list = key.map {|k| "@values[#{k.inspect}]"}
|
108
|
+
block = eval("proc {@pk ||= [#{exp_list.join(',')}]}")
|
109
|
+
class_def(:pk, &block)
|
110
|
+
|
111
|
+
exp_list = key.map {|k| "#{k.inspect} => @values[#{k.inspect}]"}
|
112
|
+
block = eval("proc {@this ||= {#{exp_list.join(',')}}}")
|
113
|
+
class_def(:pk_hash, &block)
|
114
|
+
|
115
|
+
exp_list = key.map {|k| '#{@values[%s]}' % k.inspect}.join(',')
|
116
|
+
block = eval('proc {@cache_key ||= "#{self.class}:%s"}' % exp_list)
|
117
|
+
class_def(:cache_key, &block)
|
118
|
+
|
119
|
+
meta_def(:primary_key_hash) do |v|
|
120
|
+
key.inject({}) {|m, i| m[i] = v.shift; m}
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def self.no_primary_key #:nodoc:
|
126
|
+
meta_def(:primary_key) {nil}
|
127
|
+
meta_def(:primary_key_hash) {|v| raise Error, "#{self} does not have a primary key"}
|
128
|
+
class_def(:this) {raise Error, "No primary key is associated with this model"}
|
129
|
+
class_def(:pk) {raise Error, "No primary key is associated with this model"}
|
130
|
+
class_def(:pk_hash) {raise Error, "No primary key is associated with this model"}
|
131
|
+
class_def(:cache_key) {raise Error, "No primary key is associated with this model"}
|
132
|
+
end
|
133
|
+
|
134
|
+
# Creates new instance with values set to passed-in Hash ensuring that
|
135
|
+
# new? returns true.
|
136
|
+
def self.create(values = {}, &block)
|
137
|
+
db.transaction do
|
138
|
+
obj = new(values, &block)
|
139
|
+
obj.save
|
140
|
+
obj
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Updates the instance with the supplied values with support for virtual
|
145
|
+
# attributes, ignoring any values for which no setter method is available.
|
146
|
+
def update_with_params(values)
|
147
|
+
c = columns
|
148
|
+
values.each do |k, v| m = :"#{k}="
|
149
|
+
send(m, v) if c.include?(k) || respond_to?(m)
|
150
|
+
end
|
151
|
+
save_changes
|
152
|
+
end
|
153
|
+
alias_method :update_with, :update_with_params
|
154
|
+
|
155
|
+
class << self
|
156
|
+
def create_with_params(params)
|
157
|
+
record = new
|
158
|
+
record.update_with_params(params)
|
159
|
+
record
|
160
|
+
end
|
161
|
+
alias_method :create_with, :create_with_params
|
162
|
+
end
|
163
|
+
|
164
|
+
# Returns (naked) dataset bound to current instance.
|
165
|
+
def this
|
166
|
+
@this ||= self.class.dataset.filter(:id => @values[:id]).limit(1).naked
|
167
|
+
end
|
168
|
+
|
169
|
+
# Returns a key unique to the underlying record for caching
|
170
|
+
def cache_key
|
171
|
+
pk = @values[:id] || (raise Error, 'no primary key for this record')
|
172
|
+
@cache_key ||= "#{self.class}:#{pk}"
|
173
|
+
end
|
174
|
+
|
175
|
+
# Returns primary key column(s) for object's Model class.
|
176
|
+
def primary_key
|
177
|
+
@primary_key ||= self.class.primary_key
|
178
|
+
end
|
179
|
+
|
180
|
+
# Returns the primary key value identifying the model instance. If the
|
181
|
+
# model's primary key is changed (using #set_primary_key or #no_primary_key)
|
182
|
+
# this method is redefined accordingly.
|
183
|
+
def pk
|
184
|
+
@pk ||= @values[:id]
|
185
|
+
end
|
186
|
+
|
187
|
+
# Returns a hash identifying the model instance. Stock implementation.
|
188
|
+
def pk_hash
|
189
|
+
@pk_hash ||= {:id => @values[:id]}
|
190
|
+
end
|
191
|
+
|
192
|
+
# Creates new instance with values set to passed-in Hash.
|
193
|
+
#
|
194
|
+
# This method guesses whether the record exists when
|
195
|
+
# <tt>new_record</tt> is set to false.
|
196
|
+
def initialize(values = nil, from_db = false, &block)
|
197
|
+
@changed_columns = []
|
198
|
+
unless from_db
|
199
|
+
@values = {}
|
200
|
+
if values
|
201
|
+
values.each do |k, v| m = :"#{k}="
|
202
|
+
if respond_to?(m)
|
203
|
+
send(m, v)
|
204
|
+
values.delete(k)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
values.inject(@values) {|m, kv| m[kv[0].to_sym] = kv[1]; m}
|
208
|
+
# @values.merge!(values)
|
209
|
+
end
|
210
|
+
else
|
211
|
+
@values = values || {}
|
212
|
+
end
|
213
|
+
|
214
|
+
k = primary_key
|
215
|
+
@new = !from_db
|
216
|
+
|
217
|
+
block[self] if block
|
218
|
+
after_initialize
|
219
|
+
end
|
220
|
+
|
221
|
+
# Initializes a model instance as an existing record. This constructor is
|
222
|
+
# used by Sequel to initialize model instances when fetching records.
|
223
|
+
def self.load(values)
|
224
|
+
new(values, true)
|
225
|
+
end
|
226
|
+
|
227
|
+
# Returns true if the current instance represents a new record.
|
228
|
+
def new?
|
229
|
+
@new
|
230
|
+
end
|
231
|
+
alias :new_record? :new?
|
232
|
+
|
233
|
+
# Returns true when current instance exists, false otherwise.
|
234
|
+
def exists?
|
235
|
+
this.count > 0
|
236
|
+
end
|
237
|
+
|
238
|
+
# Creates or updates the associated record. This method can also
|
239
|
+
# accept a list of specific columns to update.
|
240
|
+
def save(*columns)
|
241
|
+
before_save
|
242
|
+
if @new
|
243
|
+
before_create
|
244
|
+
iid = model.dataset.insert(@values)
|
245
|
+
# if we have a regular primary key and it's not set in @values,
|
246
|
+
# we assume it's the last inserted id
|
247
|
+
if (pk = primary_key) && !(Array === pk) && !@values[pk]
|
248
|
+
@values[pk] = iid
|
249
|
+
end
|
250
|
+
if pk
|
251
|
+
@this = nil # remove memoized this dataset
|
252
|
+
refresh
|
253
|
+
end
|
254
|
+
@new = false
|
255
|
+
after_create
|
256
|
+
else
|
257
|
+
before_update
|
258
|
+
if columns.empty?
|
259
|
+
this.update(@values)
|
260
|
+
@changed_columns = []
|
261
|
+
else # update only the specified columns
|
262
|
+
this.update(@values.reject {|k, v| !columns.include?(k)})
|
263
|
+
@changed_columns.reject! {|c| columns.include?(c)}
|
264
|
+
end
|
265
|
+
after_update
|
266
|
+
end
|
267
|
+
after_save
|
268
|
+
self
|
269
|
+
end
|
270
|
+
|
271
|
+
# Saves only changed columns or does nothing if no columns are marked as
|
272
|
+
# chanaged.
|
273
|
+
def save_changes
|
274
|
+
save(*@changed_columns) unless @changed_columns.empty?
|
275
|
+
end
|
276
|
+
|
277
|
+
# Updates and saves values to database from the passed-in Hash.
|
278
|
+
def set(values)
|
279
|
+
v = values.inject({}) {|m, kv| m[kv[0].to_sym] = kv[1]; m}
|
280
|
+
this.update(v)
|
281
|
+
v.each {|k, v| @values[k] = v}
|
282
|
+
end
|
283
|
+
alias_method :update, :set
|
284
|
+
|
285
|
+
# Reloads values from database and returns self.
|
286
|
+
def refresh
|
287
|
+
@values = this.first || raise(Error, "Record not found")
|
288
|
+
self
|
289
|
+
end
|
290
|
+
alias_method :reload, :refresh
|
291
|
+
|
292
|
+
# Like delete but runs hooks before and after delete.
|
293
|
+
def destroy
|
294
|
+
db.transaction do
|
295
|
+
before_destroy
|
296
|
+
delete
|
297
|
+
after_destroy
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
# Deletes and returns self.
|
302
|
+
def delete
|
303
|
+
this.delete
|
304
|
+
self
|
305
|
+
end
|
306
|
+
|
307
|
+
ATTR_RE = /^([a-zA-Z_]\w*)(=)?$/.freeze
|
308
|
+
EQUAL_SIGN = '='.freeze
|
309
|
+
|
310
|
+
def method_missing(m, *args) #:nodoc:
|
311
|
+
if m.to_s =~ ATTR_RE
|
312
|
+
att = $1.to_sym
|
313
|
+
write = $2 == EQUAL_SIGN
|
314
|
+
|
315
|
+
# check whether the column is legal
|
316
|
+
unless @values.has_key?(att) || columns.include?(att)
|
317
|
+
raise Error, "Invalid column (#{att.inspect}) for #{self}"
|
318
|
+
end
|
319
|
+
|
320
|
+
# define the column accessor
|
321
|
+
Thread.exclusive do
|
322
|
+
if write
|
323
|
+
model.class_def(m) {|v| self[att] = v}
|
324
|
+
else
|
325
|
+
model.class_def(m) {self[att]}
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
# call the accessor
|
330
|
+
respond_to?(m) ? send(m, *args) : super(m, *args)
|
331
|
+
else
|
332
|
+
super(m, *args)
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|