sequel_model 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/CHANGELOG +3 -0
- data/COPYING +18 -0
- data/README +251 -0
- data/Rakefile +146 -0
- data/lib/sequel_model.rb +326 -0
- data/lib/sequel_model/base.rb +97 -0
- data/lib/sequel_model/caching.rb +42 -0
- data/lib/sequel_model/hooks.rb +122 -0
- data/lib/sequel_model/plugins.rb +44 -0
- data/lib/sequel_model/pretty_table.rb +73 -0
- data/lib/sequel_model/record.rb +309 -0
- data/lib/sequel_model/relations.rb +107 -0
- data/lib/sequel_model/schema.rb +52 -0
- data/lib/sequel_model/validations.rb +117 -0
- data/spec/base_spec.rb +150 -0
- data/spec/caching_spec.rb +150 -0
- data/spec/hooks_spec.rb +107 -0
- data/spec/model_spec.rb +564 -0
- data/spec/plugins_spec.rb +61 -0
- data/spec/rcov.opts +4 -0
- data/spec/record_spec.rb +362 -0
- data/spec/relations_spec.rb +150 -0
- data/spec/schema_spec.rb +82 -0
- data/spec/spec.opts +5 -0
- data/spec/spec_helper.rb +36 -0
- data/spec/validations_spec.rb +294 -0
- metadata +111 -0
@@ -0,0 +1,97 @@
|
|
1
|
+
module Sequel
|
2
|
+
class Model
|
3
|
+
# Returns the database associated with the Model class.
|
4
|
+
def self.db
|
5
|
+
@db ||= (superclass != Object) && superclass.db or
|
6
|
+
raise Error, "No database associated with #{self}"
|
7
|
+
end
|
8
|
+
|
9
|
+
# Sets the database associated with the Model class.
|
10
|
+
def self.db=(db)
|
11
|
+
@db = db
|
12
|
+
end
|
13
|
+
|
14
|
+
# Called when a database is opened in order to automatically associate the
|
15
|
+
# first opened database with model classes.
|
16
|
+
def self.database_opened(db)
|
17
|
+
@db = db if (self == Model) && !@db
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns the dataset associated with the Model class.
|
21
|
+
def self.dataset
|
22
|
+
@dataset || super_dataset or
|
23
|
+
raise Error, "No dataset associated with #{self}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.super_dataset # :nodoc:
|
27
|
+
superclass.dataset if superclass and superclass.respond_to? :dataset
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns the columns in the result set in their original order.
|
31
|
+
#
|
32
|
+
# See Dataset#columns for more information.
|
33
|
+
def self.columns
|
34
|
+
@columns ||= @dataset.columns or
|
35
|
+
raise Error, "Could not fetch columns for #{self}"
|
36
|
+
end
|
37
|
+
|
38
|
+
# Sets the dataset associated with the Model class.
|
39
|
+
def self.set_dataset(ds)
|
40
|
+
@db = ds.db
|
41
|
+
@dataset = ds
|
42
|
+
@dataset.set_model(self)
|
43
|
+
@dataset.transform(@transform) if @transform
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns the database assoiated with the object's Model class.
|
47
|
+
def db
|
48
|
+
@db ||= model.db
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns the dataset assoiated with the object's Model class.
|
52
|
+
#
|
53
|
+
# See Dataset for more information.
|
54
|
+
def dataset
|
55
|
+
model.dataset
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns the columns associated with the object's Model class.
|
59
|
+
def columns
|
60
|
+
model.columns
|
61
|
+
end
|
62
|
+
|
63
|
+
# Serializes column with YAML or through marshalling.
|
64
|
+
def self.serialize(*columns)
|
65
|
+
format = columns.pop[:format] if Hash === columns.last
|
66
|
+
format ||= :yaml
|
67
|
+
|
68
|
+
@transform = columns.inject({}) do |m, c|
|
69
|
+
m[c] = format
|
70
|
+
m
|
71
|
+
end
|
72
|
+
@dataset.transform(@transform) if @dataset
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Lets you create a Model class with its table name already set or reopen
|
77
|
+
# an existing Model.
|
78
|
+
#
|
79
|
+
# Makes given dataset inherited.
|
80
|
+
#
|
81
|
+
# === Example:
|
82
|
+
# class Comment < Sequel::Model(:comments)
|
83
|
+
# table_name # => :comments
|
84
|
+
#
|
85
|
+
# # ...
|
86
|
+
#
|
87
|
+
# end
|
88
|
+
def self.Model(source)
|
89
|
+
@models ||= {}
|
90
|
+
@models[source] ||= Class.new(Sequel::Model) do
|
91
|
+
meta_def(:inherited) do |c|
|
92
|
+
c.set_dataset(source.is_a?(Dataset) ? source : c.db[source])
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
@@ -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,122 @@
|
|
1
|
+
module Sequel
|
2
|
+
class Model
|
3
|
+
# This Hash translates verbs to methodnames used in chain manipulation
|
4
|
+
# methods.
|
5
|
+
VERB_TO_METHOD = {:prepend => :unshift, :append => :push}
|
6
|
+
|
7
|
+
# Returns @hooks which is an instance of Hash with its hook identifier
|
8
|
+
# (Symbol) as key and the chain of hooks (Array) as value.
|
9
|
+
#
|
10
|
+
# If it is not already set it'll be with an empty set of hooks.
|
11
|
+
# This behaviour will change in the future to allow inheritance.
|
12
|
+
#
|
13
|
+
# For the time being, you should be able to do:
|
14
|
+
#
|
15
|
+
# class A < Sequel::Model(:a)
|
16
|
+
# before_save { 'Do something...' }
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# class B < A
|
20
|
+
# @hooks = superclass.hooks.clone
|
21
|
+
# before_save # => [#<Proc:0x0000c6e8@(example.rb):123>]
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# In this case you should remember that the clone doesn't create any new
|
25
|
+
# instances of your chains, so if you change the chain here it changes in
|
26
|
+
# its superclass, too.
|
27
|
+
def self.hooks
|
28
|
+
@hooks ||= Hash.new { |h, k| h[k] = [] }
|
29
|
+
end
|
30
|
+
|
31
|
+
# Adds block to chain of Hooks for <tt>:before_save</tt>.
|
32
|
+
# It can either be prepended (default) or appended.
|
33
|
+
#
|
34
|
+
# Returns the chain itself.
|
35
|
+
#
|
36
|
+
# Valid verbs are <tt>:prepend</tt> and <tt>:append</tt>.
|
37
|
+
def self.before_save(verb = :prepend, &block)
|
38
|
+
hooks[:before_save].send VERB_TO_METHOD.fetch(verb), block if block
|
39
|
+
hooks[:before_save]
|
40
|
+
end
|
41
|
+
# Adds block to chain of Hooks for <tt>:before_create</tt>.
|
42
|
+
# It can either be prepended (default) or appended.
|
43
|
+
#
|
44
|
+
# Returns the chain itself.
|
45
|
+
#
|
46
|
+
# Valid verbs are <tt>:prepend</tt> and <tt>:append</tt>.
|
47
|
+
def self.before_create(verb = :prepend, &block)
|
48
|
+
hooks[:before_create].send VERB_TO_METHOD.fetch(verb), block if block
|
49
|
+
hooks[:before_create]
|
50
|
+
end
|
51
|
+
# Adds block to chain of Hooks for <tt>:before_update</tt>.
|
52
|
+
# It can either be prepended (default) or appended.
|
53
|
+
#
|
54
|
+
# Returns the chain itself.
|
55
|
+
#
|
56
|
+
# Valid verbs are <tt>:prepend</tt> and <tt>:append</tt>.
|
57
|
+
def self.before_update(verb = :prepend, &block)
|
58
|
+
hooks[:before_update].send VERB_TO_METHOD.fetch(verb), block if block
|
59
|
+
hooks[:before_update]
|
60
|
+
end
|
61
|
+
# Adds block to chain of Hooks for <tt>:before_destroy</tt>.
|
62
|
+
# It can either be prepended (default) or appended.
|
63
|
+
#
|
64
|
+
# Returns the chain itself.
|
65
|
+
#
|
66
|
+
# Valid verbs are <tt>:prepend</tt> and <tt>:append</tt>.
|
67
|
+
def self.before_destroy(verb = :prepend, &block)
|
68
|
+
hooks[:before_destroy].send VERB_TO_METHOD.fetch(verb), block if block
|
69
|
+
hooks[:before_destroy]
|
70
|
+
end
|
71
|
+
|
72
|
+
# Adds block to chain of Hooks for <tt>:after_save</tt>.
|
73
|
+
# It can either be prepended or appended (default).
|
74
|
+
#
|
75
|
+
# Returns the chain itself.
|
76
|
+
#
|
77
|
+
# Valid verbs are <tt>:prepend</tt> and <tt>:append</tt>.
|
78
|
+
def self.after_save(verb = :append, &block)
|
79
|
+
hooks[:after_save].send VERB_TO_METHOD.fetch(verb), block if block
|
80
|
+
hooks[:after_save]
|
81
|
+
end
|
82
|
+
# Adds block to chain of Hooks for <tt>:after_create</tt>.
|
83
|
+
# It can either be prepended or appended (default).
|
84
|
+
#
|
85
|
+
# Returns the chain itself.
|
86
|
+
#
|
87
|
+
# Valid verbs are <tt>:prepend</tt> and <tt>:append</tt>.
|
88
|
+
def self.after_create(verb = :append, &block)
|
89
|
+
hooks[:after_create].send VERB_TO_METHOD.fetch(verb), block if block
|
90
|
+
hooks[:after_create]
|
91
|
+
end
|
92
|
+
# Adds block to chain of Hooks for <tt>:after_update</tt>.
|
93
|
+
# It can either be prepended or appended (default).
|
94
|
+
#
|
95
|
+
# Returns the chain itself.
|
96
|
+
#
|
97
|
+
# Valid verbs are <tt>:prepend</tt> and <tt>:append</tt>.
|
98
|
+
def self.after_update(verb = :append, &block)
|
99
|
+
hooks[:after_update].send VERB_TO_METHOD.fetch(verb), block if block
|
100
|
+
hooks[:after_update]
|
101
|
+
end
|
102
|
+
# Adds block to chain of Hooks for <tt>:after_destroy</tt>.
|
103
|
+
# It can either be prepended or appended (default).
|
104
|
+
#
|
105
|
+
# Returns the chain itself.
|
106
|
+
#
|
107
|
+
# Valid verbs are <tt>:prepend</tt> and <tt>:append</tt>.
|
108
|
+
def self.after_destroy(verb = :append, &block)
|
109
|
+
hooks[:after_destroy].send VERB_TO_METHOD.fetch(verb), block if block
|
110
|
+
hooks[:after_destroy]
|
111
|
+
end
|
112
|
+
|
113
|
+
# Evaluates specified chain of Hooks through <tt>instance_eval</tt>.
|
114
|
+
def run_hooks(key)
|
115
|
+
model.hooks[key].each {|h| instance_eval(&h)}
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.has_hooks?(key)
|
119
|
+
hooks[key] && !hooks[key].empty?
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,44 @@
|
|
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
|
+
dataset.meta_def(:"#{plugin}_opts") {args.first}
|
23
|
+
dataset.metaclass.send(:include, m::DatasetMethods)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
alias_method :is_a, :is
|
27
|
+
|
28
|
+
# Returns the module for the specified plugin. If the module is not
|
29
|
+
# defined, the corresponding plugin gem is automatically loaded.
|
30
|
+
def plugin_module(plugin)
|
31
|
+
module_name = plugin.to_s.gsub(/(^|_)(.)/) {$2.upcase}
|
32
|
+
if not Sequel::Plugins.const_defined?(module_name)
|
33
|
+
require plugin_gem(plugin)
|
34
|
+
end
|
35
|
+
Sequel::Plugins.const_get(module_name)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns the gem name for the given plugin.
|
39
|
+
def plugin_gem(plugin)
|
40
|
+
"sequel_#{plugin}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
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,309 @@
|
|
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
|
+
@values[column] = value
|
13
|
+
@changed_columns << column unless @changed_columns.include?(column)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Enumerates through all attributes.
|
17
|
+
#
|
18
|
+
# === Example:
|
19
|
+
# Ticket.find(7).each { |k, v| puts "#{k} => #{v}" }
|
20
|
+
def each(&block)
|
21
|
+
@values.each(&block)
|
22
|
+
end
|
23
|
+
# Returns attribute names.
|
24
|
+
def keys
|
25
|
+
@values.keys
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns value for <tt>:id</tt> attribute.
|
29
|
+
def id
|
30
|
+
@values[:id]
|
31
|
+
end
|
32
|
+
|
33
|
+
# Compares model instances by values.
|
34
|
+
def ==(obj)
|
35
|
+
(obj.class == model) && (obj.values == @values)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Compares model instances by pkey.
|
39
|
+
def ===(obj)
|
40
|
+
(obj.class == model) && (obj.pk == pk)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns key for primary key.
|
44
|
+
def self.primary_key
|
45
|
+
:id
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns primary key attribute hash.
|
49
|
+
def self.primary_key_hash(value)
|
50
|
+
{:id => value}
|
51
|
+
end
|
52
|
+
|
53
|
+
# Sets primary key, regular and composite are possible.
|
54
|
+
#
|
55
|
+
# == Example:
|
56
|
+
# class Tagging < Sequel::Model(:taggins)
|
57
|
+
# # composite key
|
58
|
+
# set_primary_key :taggable_id, :tag_id
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# class Person < Sequel::Model(:person)
|
62
|
+
# # regular key
|
63
|
+
# set_primary_key :person_id
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# <i>You can even set it to nil!</i>
|
67
|
+
def self.set_primary_key(*key)
|
68
|
+
# if k is nil, we go to no_primary_key
|
69
|
+
if key.empty? || (key.size == 1 && key.first == nil)
|
70
|
+
return no_primary_key
|
71
|
+
end
|
72
|
+
|
73
|
+
# backwards compat
|
74
|
+
key = (key.length == 1) ? key[0] : key.flatten
|
75
|
+
|
76
|
+
# redefine primary_key
|
77
|
+
meta_def(:primary_key) {key}
|
78
|
+
|
79
|
+
unless key.is_a? Array # regular primary key
|
80
|
+
class_def(:this) do
|
81
|
+
@this ||= dataset.filter(key => @values[key]).limit(1).naked
|
82
|
+
end
|
83
|
+
class_def(:pk) do
|
84
|
+
@pk ||= @values[key]
|
85
|
+
end
|
86
|
+
class_def(:pk_hash) do
|
87
|
+
@pk ||= {key => @values[key]}
|
88
|
+
end
|
89
|
+
class_def(:cache_key) do
|
90
|
+
pk = @values[key] || (raise Error, 'no primary key for this record')
|
91
|
+
@cache_key ||= "#{self.class}:#{pk}"
|
92
|
+
end
|
93
|
+
meta_def(:primary_key_hash) do |v|
|
94
|
+
{key => v}
|
95
|
+
end
|
96
|
+
else # composite key
|
97
|
+
exp_list = key.map {|k| "#{k.inspect} => @values[#{k.inspect}]"}
|
98
|
+
block = eval("proc {@this ||= self.class.dataset.filter(#{exp_list.join(',')}).limit(1).naked}")
|
99
|
+
class_def(:this, &block)
|
100
|
+
|
101
|
+
exp_list = key.map {|k| "@values[#{k.inspect}]"}
|
102
|
+
block = eval("proc {@pk ||= [#{exp_list.join(',')}]}")
|
103
|
+
class_def(:pk, &block)
|
104
|
+
|
105
|
+
exp_list = key.map {|k| "#{k.inspect} => @values[#{k.inspect}]"}
|
106
|
+
block = eval("proc {@this ||= {#{exp_list.join(',')}}}")
|
107
|
+
class_def(:pk_hash, &block)
|
108
|
+
|
109
|
+
exp_list = key.map {|k| '#{@values[%s]}' % k.inspect}.join(',')
|
110
|
+
block = eval('proc {@cache_key ||= "#{self.class}:%s"}' % exp_list)
|
111
|
+
class_def(:cache_key, &block)
|
112
|
+
|
113
|
+
meta_def(:primary_key_hash) do |v|
|
114
|
+
key.inject({}) {|m, i| m[i] = v.shift; m}
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.no_primary_key #:nodoc:
|
120
|
+
meta_def(:primary_key) {nil}
|
121
|
+
meta_def(:primary_key_hash) {|v| raise Error, "#{self} does not have a primary key"}
|
122
|
+
class_def(:this) {raise Error, "No primary key is associated with this model"}
|
123
|
+
class_def(:pk) {raise Error, "No primary key is associated with this model"}
|
124
|
+
class_def(:pk_hash) {raise Error, "No primary key is associated with this model"}
|
125
|
+
class_def(:cache_key) {raise Error, "No primary key is associated with this model"}
|
126
|
+
end
|
127
|
+
|
128
|
+
# Creates new instance with values set to passed-in Hash ensuring that
|
129
|
+
# new? returns true.
|
130
|
+
def self.create(values = {})
|
131
|
+
db.transaction do
|
132
|
+
obj = new(values, true)
|
133
|
+
obj.save
|
134
|
+
obj
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
class << self
|
139
|
+
def create_with_params(params)
|
140
|
+
create(params.reject {|k, v| !columns.include?(k.to_sym)})
|
141
|
+
end
|
142
|
+
alias_method :create_with, :create_with_params
|
143
|
+
end
|
144
|
+
|
145
|
+
# Returns (naked) dataset bound to current instance.
|
146
|
+
def this
|
147
|
+
@this ||= self.class.dataset.filter(:id => @values[:id]).limit(1).naked
|
148
|
+
end
|
149
|
+
|
150
|
+
# Returns a key unique to the underlying record for caching
|
151
|
+
def cache_key
|
152
|
+
pk = @values[:id] || (raise Error, 'no primary key for this record')
|
153
|
+
@cache_key ||= "#{self.class}:#{pk}"
|
154
|
+
end
|
155
|
+
|
156
|
+
# Returns primary key column(s) for object's Model class.
|
157
|
+
def primary_key
|
158
|
+
@primary_key ||= self.class.primary_key
|
159
|
+
end
|
160
|
+
|
161
|
+
# Returns value for primary key.
|
162
|
+
def pkey
|
163
|
+
warn "Model#pkey is deprecated. Please use Model#pk instead."
|
164
|
+
@pkey ||= @values[self.class.primary_key]
|
165
|
+
end
|
166
|
+
|
167
|
+
# Returns the primary key value identifying the model instance. Stock implementation.
|
168
|
+
def pk
|
169
|
+
@pk ||= @values[:id]
|
170
|
+
end
|
171
|
+
|
172
|
+
# Returns a hash identifying the model instance. Stock implementation.
|
173
|
+
def pk_hash
|
174
|
+
@pk_hash ||= {:id => @values[:id]}
|
175
|
+
end
|
176
|
+
|
177
|
+
# Creates new instance with values set to passed-in Hash.
|
178
|
+
#
|
179
|
+
# This method guesses whether the record exists when
|
180
|
+
# <tt>new_record</tt> is set to false.
|
181
|
+
def initialize(values = {}, new_record = false, &block)
|
182
|
+
@values = values
|
183
|
+
@changed_columns = []
|
184
|
+
|
185
|
+
@new = new_record
|
186
|
+
unless @new # determine if it's a new record
|
187
|
+
k = self.class.primary_key
|
188
|
+
# if there's no primary key for the model class, or
|
189
|
+
# @values doesn't contain a primary key value, then
|
190
|
+
# we regard this instance as new.
|
191
|
+
@new = (k == nil) || (!(Array === k) && !@values[k])
|
192
|
+
end
|
193
|
+
|
194
|
+
block[self] if block
|
195
|
+
end
|
196
|
+
|
197
|
+
# Returns true if the current instance represents a new record.
|
198
|
+
def new?
|
199
|
+
@new
|
200
|
+
end
|
201
|
+
alias :new_record? :new?
|
202
|
+
|
203
|
+
# Returns true when current instance exists, false otherwise.
|
204
|
+
def exists?
|
205
|
+
this.count > 0
|
206
|
+
end
|
207
|
+
|
208
|
+
# Creates or updates the associated record. This method can also
|
209
|
+
# accept a list of specific columns to update.
|
210
|
+
def save(*columns)
|
211
|
+
run_hooks(:before_save)
|
212
|
+
if @new
|
213
|
+
run_hooks(:before_create)
|
214
|
+
iid = model.dataset.insert(@values)
|
215
|
+
# if we have a regular primary key and it's not set in @values,
|
216
|
+
# we assume it's the last inserted id
|
217
|
+
if (pk = primary_key) && !(Array === pk) && !@values[pk]
|
218
|
+
@values[pk] = iid
|
219
|
+
end
|
220
|
+
if pk
|
221
|
+
@this = nil # remove memoized this dataset
|
222
|
+
refresh
|
223
|
+
end
|
224
|
+
@new = false
|
225
|
+
run_hooks(:after_create)
|
226
|
+
else
|
227
|
+
run_hooks(:before_update)
|
228
|
+
if columns.empty?
|
229
|
+
this.update(@values)
|
230
|
+
@changed_columns = []
|
231
|
+
else # update only the specified columns
|
232
|
+
this.update(@values.reject {|k, v| !columns.include?(k)})
|
233
|
+
@changed_columns.reject! {|c| columns.include?(c)}
|
234
|
+
end
|
235
|
+
run_hooks(:after_update)
|
236
|
+
end
|
237
|
+
run_hooks(:after_save)
|
238
|
+
self
|
239
|
+
end
|
240
|
+
|
241
|
+
# Saves only changed columns or does nothing if no columns are marked as
|
242
|
+
# chanaged.
|
243
|
+
def save_changes
|
244
|
+
save(*@changed_columns) unless @changed_columns.empty?
|
245
|
+
end
|
246
|
+
|
247
|
+
# Updates and saves values to database from the passed-in Hash.
|
248
|
+
def set(values)
|
249
|
+
this.update(values)
|
250
|
+
values.each {|k, v| @values[k] = v}
|
251
|
+
end
|
252
|
+
alias_method :update, :set
|
253
|
+
|
254
|
+
# Reloads values from database and returns self.
|
255
|
+
def refresh
|
256
|
+
@values = this.first || raise(Error, "Record not found")
|
257
|
+
self
|
258
|
+
end
|
259
|
+
|
260
|
+
# Like delete but runs hooks before and after delete.
|
261
|
+
def destroy
|
262
|
+
db.transaction do
|
263
|
+
run_hooks(:before_destroy)
|
264
|
+
delete
|
265
|
+
run_hooks(:after_destroy)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
# Deletes and returns self.
|
270
|
+
def delete
|
271
|
+
this.delete
|
272
|
+
self
|
273
|
+
end
|
274
|
+
|
275
|
+
ATTR_RE = /^([a-zA-Z_]\w*)(=)?$/.freeze
|
276
|
+
|
277
|
+
def method_missing(m, *args) #:nodoc:
|
278
|
+
if m.to_s =~ ATTR_RE
|
279
|
+
att = $1.to_sym
|
280
|
+
write = $2 == '='
|
281
|
+
|
282
|
+
# check whether the column is legal
|
283
|
+
unless columns.include?(att)
|
284
|
+
# if read accessor and a value exists for the column, we return it
|
285
|
+
if !write && @values.has_key?(att)
|
286
|
+
return @values[att]
|
287
|
+
end
|
288
|
+
|
289
|
+
# otherwise, raise an error
|
290
|
+
raise Error, "Invalid column (#{att.inspect}) for #{self}"
|
291
|
+
end
|
292
|
+
|
293
|
+
# define the column accessor
|
294
|
+
Thread.exclusive do
|
295
|
+
if write
|
296
|
+
model.class_def(m) {|v| self[att] = v}
|
297
|
+
else
|
298
|
+
model.class_def(m) {self[att]}
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
# call the accessor
|
303
|
+
respond_to?(m) ? send(m, *args) : super(m, *args)
|
304
|
+
else
|
305
|
+
super(m, *args)
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|