sequel 1.5.1 → 2.0.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 +30 -0
- data/README +12 -15
- data/Rakefile +9 -20
- data/lib/sequel_model.rb +47 -72
- data/lib/sequel_model/association_reflection.rb +59 -0
- data/lib/sequel_model/associations.rb +99 -94
- data/lib/sequel_model/base.rb +308 -102
- data/lib/sequel_model/caching.rb +72 -27
- data/lib/sequel_model/eager_loading.rb +308 -300
- data/lib/sequel_model/hooks.rb +51 -49
- data/lib/sequel_model/inflector.rb +186 -182
- data/lib/sequel_model/plugins.rb +54 -40
- data/lib/sequel_model/record.rb +185 -220
- data/lib/sequel_model/schema.rb +27 -34
- data/lib/sequel_model/validations.rb +54 -73
- data/spec/association_reflection_spec.rb +85 -0
- data/spec/associations_spec.rb +160 -73
- data/spec/base_spec.rb +3 -3
- data/spec/eager_loading_spec.rb +132 -35
- data/spec/hooks_spec.rb +120 -20
- data/spec/inflector_spec.rb +2 -2
- data/spec/model_spec.rb +110 -78
- data/spec/plugins_spec.rb +4 -0
- data/spec/rcov.opts +1 -1
- data/spec/record_spec.rb +160 -59
- data/spec/spec.opts +0 -5
- data/spec/spec_helper.rb +12 -0
- data/spec/validations_spec.rb +23 -0
- metadata +60 -50
- data/lib/sequel_model/deprecated.rb +0 -81
- data/lib/sequel_model/inflections.rb +0 -112
- data/spec/deprecated_relations_spec.rb +0 -113
data/CHANGELOG
CHANGED
@@ -1,3 +1,33 @@
|
|
1
|
+
=== 2.0.0 (2008-06-01)
|
2
|
+
|
3
|
+
* Comprehensive update of all documentation (jeremyevans)
|
4
|
+
|
5
|
+
* Remove methods deprecated in 1.5.0 (jeremyevans)
|
6
|
+
|
7
|
+
* Add typecasting on attribute assignment to Sequel::Model objects, optional but enabled by default (jeremyevans)
|
8
|
+
|
9
|
+
* Returning false in one of the before_ hooks now causes the appropriate method(s) to immediately return false (jeremyevans)
|
10
|
+
|
11
|
+
* Add remove_all_* association method for *_to_many associations, which removes the association with all currently associated objects (jeremyevans)
|
12
|
+
|
13
|
+
* Add Model.lazy_load_schema=, when set to true, it loads the schema on first instantiation (jeremyevans)
|
14
|
+
|
15
|
+
* Add before_validation and after_validation hooks, called whenever the model is validated (jeremyevans)
|
16
|
+
|
17
|
+
* Add Model.default_foreign_key, a private class method that allows changing the default foreign key that Sequel will use in associations (jeremyevans)
|
18
|
+
|
19
|
+
* Cache negative lookup when eagerly loading many_to_one associations (jeremyevans)
|
20
|
+
|
21
|
+
* Make all associations support the :select option, not just many_to_many (jeremyevans)
|
22
|
+
|
23
|
+
* Allow the use of blocks when eager loading, and add the :eager_block and :allow_eager association options for configuration (jeremyevans)
|
24
|
+
|
25
|
+
* Add the :graph_join_type, :graph_conditions, and :graph_join_table_conditions association options, used when eager graphing (jeremyevans)
|
26
|
+
|
27
|
+
* Add AssociationReflection class (subclass of Hash), to make calling a couple of private Model methods unnecessary (jeremyevans)
|
28
|
+
|
29
|
+
* Change hook methods so that if a tag/method is specified it overwrites an existing hook block with the same tag/method (jeremyevans)
|
30
|
+
|
1
31
|
=== 1.5.1 (2008-04-30)
|
2
32
|
|
3
33
|
* Fix Dataset#eager_graph when not all objects have associated objects (jeremyevans)
|
data/README
CHANGED
@@ -16,18 +16,13 @@ You can, however, explicitly set the table name or even the dataset used:
|
|
16
16
|
|
17
17
|
class Post < Sequel::Model(:my_posts)
|
18
18
|
end
|
19
|
-
|
20
19
|
# or:
|
21
|
-
|
22
20
|
Post.set_dataset :my_posts
|
23
|
-
|
24
21
|
# or:
|
25
|
-
|
26
22
|
Post.set_dataset DB[:my_posts].where(:category => 'ruby')
|
27
23
|
|
28
24
|
=== Resources
|
29
25
|
|
30
|
-
* {Project page}[http://code.google.com/p/ruby-sequel/]
|
31
26
|
* {Source code}[http://github.com/jeremyevans/sequel]
|
32
27
|
* {Bug tracking}[http://code.google.com/p/ruby-sequel/issues/list]
|
33
28
|
* {Google group}[http://groups.google.com/group/sequel-talk]
|
@@ -70,7 +65,7 @@ You can also define a model class that does not have a primary key, but then you
|
|
70
65
|
A model instance can also be fetched by specifying a condition:
|
71
66
|
|
72
67
|
post = Post[:title => 'hello world']
|
73
|
-
post = Post.find
|
68
|
+
post = Post.find(:num_comments < 10)
|
74
69
|
|
75
70
|
=== Iterating over records
|
76
71
|
|
@@ -80,8 +75,8 @@ A model class lets you iterate over specific records by acting as a proxy to the
|
|
80
75
|
|
81
76
|
You can also manipulate the records in the dataset:
|
82
77
|
|
83
|
-
Post.filter
|
84
|
-
Post.filter
|
78
|
+
Post.filter(:num_comments < 7).delete
|
79
|
+
Post.filter(:title.like(/ruby/)).update(:category => 'ruby')
|
85
80
|
|
86
81
|
=== Accessing record values
|
87
82
|
|
@@ -126,7 +121,7 @@ You can also supply a block to Model.new and Model.create:
|
|
126
121
|
|
127
122
|
=== Hooks
|
128
123
|
|
129
|
-
You can execute custom code when creating, updating, or deleting records by using hooks. The before_create and after_create hooks wrap record creation. The before_update and after_update wrap record updating. The before_save and after_save wrap record creation and updating. The before_destroy and after_destroy wrap destruction.
|
124
|
+
You can execute custom code when creating, updating, or deleting records by using hooks. The before_create and after_create hooks wrap record creation. The before_update and after_update wrap record updating. The before_save and after_save wrap record creation and updating. The before_destroy and after_destroy wrap destruction. The before_validation and after_validation hooks wrap validation.
|
130
125
|
|
131
126
|
Hooks are defined by supplying a block:
|
132
127
|
|
@@ -174,7 +169,7 @@ You can also use the ActiveRecord names for these associations:
|
|
174
169
|
has_and_belongs_to_many :tags
|
175
170
|
end
|
176
171
|
|
177
|
-
many_to_one
|
172
|
+
many_to_one creates a getter and setter for each model object:
|
178
173
|
|
179
174
|
class Post < Sequel::Model
|
180
175
|
many_to_one :author
|
@@ -184,7 +179,7 @@ many_to_one/belongs_to creates a getter and setter for each model object:
|
|
184
179
|
post.author = Author[:name => 'Sharon']
|
185
180
|
post.author
|
186
181
|
|
187
|
-
one_to_many
|
182
|
+
one_to_many and many_to_many create a getter method, a method for adding an object to the association, a method for removing an object from the association, and a method for removing all associated objected from the association:
|
188
183
|
|
189
184
|
class Post < Sequel::Model
|
190
185
|
one_to_many :comments
|
@@ -196,9 +191,11 @@ one_to_many/has_many and many_to_many/has_and_belongs_to_many create a getter me
|
|
196
191
|
comment = Comment.create(:text=>'hi')
|
197
192
|
post.add_comment(comment)
|
198
193
|
post.remove_comment(comment)
|
194
|
+
post.remove_all_comments
|
199
195
|
tag = Tag.create(:tag=>'interesting')
|
200
196
|
post.add_tag(tag)
|
201
197
|
post.remove_tag(tag)
|
198
|
+
post.remove_all_tags
|
202
199
|
|
203
200
|
=== Eager Loading
|
204
201
|
|
@@ -229,7 +226,7 @@ Associations can be eagerly loaded via .eager and the :eager association option.
|
|
229
226
|
Post.eager(:person).all
|
230
227
|
|
231
228
|
# eager is a dataset method, so it works with filters/orders/limits/etc.
|
232
|
-
Post.filter(
|
229
|
+
Post.filter(:topic > 'M').order(:date).limit(5).eager(:person).all
|
233
230
|
|
234
231
|
person = Person.first
|
235
232
|
# Eager loading via :eager (will eagerly load the tags for this person's posts)
|
@@ -273,7 +270,7 @@ The obvious way to add table-wide logic is to define class methods to the model
|
|
273
270
|
|
274
271
|
class Post < Sequel::Model
|
275
272
|
def self.posts_with_few_comments
|
276
|
-
filter
|
273
|
+
filter(:num_comments < 30)
|
277
274
|
end
|
278
275
|
|
279
276
|
def self.clean_posts_with_few_comments
|
@@ -285,7 +282,7 @@ You can also implement table-wide logic by defining methods on the dataset:
|
|
285
282
|
|
286
283
|
class Post < Sequel::Model
|
287
284
|
def_dataset_method(:posts_with_few_comments) do
|
288
|
-
filter
|
285
|
+
filter(:num_comments < 30)
|
289
286
|
end
|
290
287
|
|
291
288
|
def_dataset_method(:clean_posts_with_few_comments) do
|
@@ -300,7 +297,7 @@ This is the recommended way of implementing table-wide operations, and allows yo
|
|
300
297
|
Sequel models also provide a short hand notation for filters:
|
301
298
|
|
302
299
|
class Post < Sequel::Model
|
303
|
-
subset(:posts_with_few_comments
|
300
|
+
subset(:posts_with_few_comments, :num_comments < 30)
|
304
301
|
subset :invisible, :visible => false
|
305
302
|
end
|
306
303
|
|
data/Rakefile
CHANGED
@@ -9,20 +9,19 @@ include FileUtils
|
|
9
9
|
# Configuration
|
10
10
|
##############################################################################
|
11
11
|
NAME = "sequel"
|
12
|
-
VERS = "
|
13
|
-
SEQUEL_CORE_VERS= "
|
14
|
-
CLEAN.include ["**/.*.sw?", "pkg
|
15
|
-
RDOC_OPTS = ["--quiet", "--line-numbers", "--inline-source"
|
12
|
+
VERS = "2.0.0"
|
13
|
+
SEQUEL_CORE_VERS= "2.0.0"
|
14
|
+
CLEAN.include ["**/.*.sw?", "pkg", ".config", "rdoc", "coverage"]
|
15
|
+
RDOC_OPTS = ["--quiet", "--line-numbers", "--inline-source", '--title', \
|
16
|
+
'Sequel: The Database Toolkit for Ruby: Model Classes', '--main', 'README']
|
16
17
|
|
17
18
|
##############################################################################
|
18
19
|
# RDoc
|
19
20
|
##############################################################################
|
20
21
|
Rake::RDocTask.new do |rdoc|
|
21
|
-
rdoc.rdoc_dir = "
|
22
|
+
rdoc.rdoc_dir = "rdoc"
|
22
23
|
rdoc.options += RDOC_OPTS
|
23
|
-
rdoc.
|
24
|
-
rdoc.title = "Sequel: The Database Toolkit for Ruby: Model Classes"
|
25
|
-
rdoc.rdoc_files.add ["README", "COPYING", "lib/**/*.rb"]
|
24
|
+
rdoc.rdoc_files.add ["README", "COPYING", "doc/*.rdoc", "lib/**/*.rb"]
|
26
25
|
end
|
27
26
|
|
28
27
|
##############################################################################
|
@@ -37,7 +36,7 @@ spec = Gem::Specification.new do |s|
|
|
37
36
|
s.version = VERS
|
38
37
|
s.platform = Gem::Platform::RUBY
|
39
38
|
s.has_rdoc = true
|
40
|
-
s.extra_rdoc_files = ["README", "CHANGELOG", "COPYING"]
|
39
|
+
s.extra_rdoc_files = ["README", "CHANGELOG", "COPYING"] + Dir["doc/*.rdoc"]
|
41
40
|
s.rdoc_options += RDOC_OPTS + ["--exclude", "^(examples|extras)\/"]
|
42
41
|
s.summary = "The Database Toolkit for Ruby: Model Classes"
|
43
42
|
s.description = s.summary
|
@@ -45,18 +44,8 @@ spec = Gem::Specification.new do |s|
|
|
45
44
|
s.email = "code@jeremyevans.net"
|
46
45
|
s.homepage = "http://sequel.rubyforge.org"
|
47
46
|
s.required_ruby_version = ">= 1.8.4"
|
48
|
-
|
49
|
-
case RUBY_PLATFORM
|
50
|
-
when /java/
|
51
|
-
s.platform = "jruby"
|
52
|
-
else
|
53
|
-
s.platform = Gem::Platform::RUBY
|
54
|
-
end
|
55
|
-
|
56
47
|
s.add_dependency("sequel_core", "= #{SEQUEL_CORE_VERS}")
|
57
|
-
|
58
48
|
s.files = %w(COPYING README Rakefile) + Dir.glob("{doc,spec,lib}/**/*")
|
59
|
-
|
60
49
|
s.require_path = "lib"
|
61
50
|
end
|
62
51
|
|
@@ -135,7 +124,7 @@ STATS_DIRECTORIES = [
|
|
135
124
|
|
136
125
|
desc "Report code statistics (KLOCs, etc) from the application"
|
137
126
|
task :stats do
|
138
|
-
require "extra/stats"
|
127
|
+
require "../extra/stats"
|
139
128
|
verbose = true
|
140
129
|
CodeStatistics.new(*STATS_DIRECTORIES).to_s
|
141
130
|
end
|
data/lib/sequel_model.rb
CHANGED
@@ -1,82 +1,57 @@
|
|
1
1
|
require 'sequel_core'
|
2
|
+
%w"inflector base hooks record schema association_reflection
|
3
|
+
associations caching plugins validations eager_loading".each do |f|
|
4
|
+
require "sequel_model/#{f}"
|
5
|
+
end
|
2
6
|
|
3
7
|
module Sequel
|
4
|
-
|
5
|
-
|
8
|
+
# Holds the nameless subclasses that are created with
|
9
|
+
# Sequel::Model(), necessary for reopening subclasses with the
|
10
|
+
# Sequel::Model() superclass specified.
|
11
|
+
@models = {}
|
12
|
+
|
13
|
+
# Lets you create a Model subclass with its dataset already set.
|
14
|
+
# source can be an existing dataset or a symbol (in which case
|
15
|
+
# it will create a dataset using the default database with
|
16
|
+
# source as the table name.
|
17
|
+
#
|
18
|
+
# Example:
|
19
|
+
# class Comment < Sequel::Model(:something)
|
20
|
+
# table_name # => :something
|
21
|
+
# end
|
22
|
+
def self.Model(source)
|
23
|
+
return @models[source] if @models[source]
|
24
|
+
klass = Class.new(Model)
|
25
|
+
klass.set_dataset(source.is_a?(Dataset) ? source : Model.db[source])
|
26
|
+
@models[source] = klass
|
6
27
|
end
|
7
|
-
end
|
8
28
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
29
|
+
# Model has some methods that are added via metaprogramming:
|
30
|
+
#
|
31
|
+
# * All of the methods in DATASET_METHODS have class methods created that call
|
32
|
+
# the Model's dataset with the method of the same name with the given
|
33
|
+
# arguments.
|
34
|
+
# * All of the methods in HOOKS have class methods created that accept
|
35
|
+
# either a method name symbol or an optional tag and a block. These
|
36
|
+
# methods run the code as a callback at the specified time. For example:
|
37
|
+
#
|
38
|
+
# Model.before_save :do_something
|
39
|
+
# Model.before_save(:do_something_else){ self.something_else = 42}
|
40
|
+
# object = Model.new
|
41
|
+
# object.save
|
42
|
+
#
|
43
|
+
# Would run the object's :do_something method following by the code
|
44
|
+
# block related to :do_something_else. Note that if you specify a
|
45
|
+
# block, a tag is optional. If the tag is not nil, it will overwrite
|
46
|
+
# a previous block with the same tag. This allows hooks to work with
|
47
|
+
# systems that reload code.
|
48
|
+
# * All of the methods in HOOKS also create instance methods, but you
|
49
|
+
# should not override these instance methods.
|
50
|
+
# * The following instance_methods all call the class method of the same
|
51
|
+
# name: columns, dataset, db, primary_key, str_columns.
|
17
52
|
class Model
|
18
53
|
extend Enumerable
|
19
54
|
extend Associations
|
20
|
-
|
21
|
-
# the class name and values.
|
22
|
-
def inspect
|
23
|
-
"#<%s @values=%s>" % [self.class.name, @values.inspect]
|
24
|
-
end
|
25
|
-
|
26
|
-
# Defines a method that returns a filtered dataset.
|
27
|
-
def self.subset(name, *args, &block)
|
28
|
-
def_dataset_method(name){filter(*args, &block)}
|
29
|
-
end
|
30
|
-
|
31
|
-
# Finds a single record according to the supplied filter, e.g.:
|
32
|
-
#
|
33
|
-
# Ticket.find :author => 'Sharon' # => record
|
34
|
-
# Ticket.find {:price == 17} # => Dataset
|
35
|
-
#
|
36
|
-
def self.find(*args, &block)
|
37
|
-
dataset.filter(*args, &block).first
|
38
|
-
end
|
39
|
-
|
40
|
-
# TODO: doc
|
41
|
-
def self.[](*args)
|
42
|
-
args = args.first if (args.size == 1)
|
43
|
-
if args === true || args === false
|
44
|
-
raise Error::InvalidFilter, "Did you mean to supply a hash?"
|
45
|
-
end
|
46
|
-
dataset[(Hash === args) ? args : primary_key_hash(args)]
|
47
|
-
end
|
48
|
-
|
49
|
-
# TODO: doc
|
50
|
-
def self.fetch(*args)
|
51
|
-
db.fetch(*args).set_model(self)
|
52
|
-
end
|
53
|
-
|
54
|
-
# Like find but invokes create with given conditions when record does not
|
55
|
-
# exists.
|
56
|
-
def self.find_or_create(cond)
|
57
|
-
find(cond) || create(cond)
|
58
|
-
end
|
59
|
-
|
60
|
-
# Deletes all records in the model's table.
|
61
|
-
def self.delete_all
|
62
|
-
dataset.delete
|
63
|
-
end
|
64
|
-
|
65
|
-
# Like delete_all, but invokes before_destroy and after_destroy hooks if used.
|
66
|
-
def self.destroy_all
|
67
|
-
dataset.destroy
|
68
|
-
end
|
69
|
-
|
70
|
-
# Add dataset methods via metaprogramming
|
71
|
-
DATASET_METHODS = %w'all avg count delete distinct eager eager_graph each each_page
|
72
|
-
empty? except exclude filter first from_self full_outer_join graph
|
73
|
-
group group_and_count group_by having import inner_join insert
|
74
|
-
insert_multiple intersect interval invert_order join join_table last
|
75
|
-
left_outer_join limit multi_insert naked order order_by order_more
|
76
|
-
paginate print query range reverse_order right_outer_join select
|
77
|
-
select_all select_more set set_graph_aliases single_value size to_csv
|
78
|
-
transform union uniq unordered update where'
|
79
|
-
|
80
|
-
def_dataset_method *DATASET_METHODS
|
55
|
+
include Validation
|
81
56
|
end
|
82
57
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Sequel
|
2
|
+
class Model
|
3
|
+
module Associations
|
4
|
+
# AssociationReflection is a Hash subclass that keeps information on Sequel::Model associations. It
|
5
|
+
# provides a few methods to reduce the amount of internal code duplication. It should not
|
6
|
+
# be instantiated by the user.
|
7
|
+
class AssociationReflection < Hash
|
8
|
+
RECIPROCAL_ASSOCIATIONS = {:many_to_one=>:one_to_many, :one_to_many=>:many_to_one, :many_to_many=>:many_to_many}
|
9
|
+
|
10
|
+
# The class associated to the current model class via this association
|
11
|
+
def associated_class
|
12
|
+
self[:class] ||= self[:class_name].constantize
|
13
|
+
end
|
14
|
+
|
15
|
+
# The associated class's primary key (used for caching)
|
16
|
+
def associated_primary_key
|
17
|
+
self[:associated_primary_key] ||= associated_class.primary_key
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns/sets the reciprocal association variable, if one exists
|
21
|
+
def reciprocal
|
22
|
+
return self[:reciprocal] if include?(:reciprocal)
|
23
|
+
reciprocal_type = RECIPROCAL_ASSOCIATIONS[self[:type]]
|
24
|
+
if reciprocal_type == :many_to_many
|
25
|
+
left_key = self[:left_key]
|
26
|
+
right_key = self[:right_key]
|
27
|
+
join_table = self[:join_table]
|
28
|
+
associated_class.all_association_reflections.each do |assoc_reflect|
|
29
|
+
if assoc_reflect[:type] == :many_to_many && assoc_reflect[:left_key] == right_key \
|
30
|
+
&& assoc_reflect[:right_key] == left_key && assoc_reflect[:join_table] == join_table
|
31
|
+
return self[:reciprocal] = association_ivar(assoc_reflect[:name])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
else
|
35
|
+
key = self[:key]
|
36
|
+
associated_class.all_association_reflections.each do |assoc_reflect|
|
37
|
+
if assoc_reflect[:type] == reciprocal_type && assoc_reflect[:key] == key
|
38
|
+
return self[:reciprocal] = association_ivar(assoc_reflect[:name])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
self[:reciprocal] = nil
|
43
|
+
end
|
44
|
+
|
45
|
+
# The columns to select when loading the association
|
46
|
+
def select
|
47
|
+
self[:select] ||= associated_class.table_name.*
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# Name symbol of association instance variable
|
53
|
+
def association_ivar(name)
|
54
|
+
:"@#{name}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -19,6 +19,7 @@
|
|
19
19
|
# milestones, allowing for further filtering/limiting/etc.
|
20
20
|
# * add_milestone(obj) - Associates the passed milestone with this object
|
21
21
|
# * remove_milestone(obj) - Removes the association with the passed milestone
|
22
|
+
# * remove_all_milestones - Removes associations with all associated milestones
|
22
23
|
#
|
23
24
|
# By default the classes for the associations are inferred from the association
|
24
25
|
# name, so for example the Project#portfolio will return an instance of
|
@@ -27,9 +28,9 @@
|
|
27
28
|
#
|
28
29
|
# Association definitions are also reflected by the class, e.g.:
|
29
30
|
#
|
30
|
-
#
|
31
|
+
# Project.associations
|
31
32
|
# => [:portfolio, :milestones]
|
32
|
-
#
|
33
|
+
# Project.association_reflection(:portfolio)
|
33
34
|
# => {:type => :many_to_one, :name => :portfolio, :class_name => "Portfolio"}
|
34
35
|
#
|
35
36
|
# Associations can be defined by either using the associate method, or by
|
@@ -42,9 +43,7 @@
|
|
42
43
|
# one_to_many :attributes
|
43
44
|
# has_many :attributes
|
44
45
|
module Sequel::Model::Associations
|
45
|
-
|
46
|
-
|
47
|
-
# Array of all association reflections
|
46
|
+
# Array of all association reflections for this model class
|
48
47
|
def all_association_reflections
|
49
48
|
association_reflections.values
|
50
49
|
end
|
@@ -71,6 +70,8 @@ module Sequel::Model::Associations
|
|
71
70
|
#
|
72
71
|
# The following options can be supplied:
|
73
72
|
# * *ALL types*:
|
73
|
+
# - :allow_eager - If set to false, you cannot load the association eagerly
|
74
|
+
# via eager or eager_graph
|
74
75
|
# - :class - The associated class or its name. If not
|
75
76
|
# given, uses the association's name, which is camelized (and
|
76
77
|
# singularized unless the type is :many_to_one)
|
@@ -78,13 +79,25 @@ module Sequel::Model::Associations
|
|
78
79
|
# For many_to_one associations, this is ignored unless this association is
|
79
80
|
# being eagerly loaded, as it doesn't save queries unless multiple objects
|
80
81
|
# can be loaded at once.
|
82
|
+
# - :eager_block - If given, use the block instead of the default block when
|
83
|
+
# eagerly loading. To not use a block when eager loading (when one is used normally),
|
84
|
+
# set to nil.
|
85
|
+
# - :graph_conditions - The conditions to use on the SQL join when eagerly loading
|
86
|
+
# the association via eager_graph
|
87
|
+
# - :graph_join_type - The type of SQL join to use when eagerly loading the association via
|
88
|
+
# eager_graph
|
89
|
+
# - :order - the column(s) by which to order the association dataset. Can be a
|
90
|
+
# singular column or an array.
|
81
91
|
# - :reciprocal - the symbol name of the instance variable of the reciprocal association,
|
82
92
|
# if it exists. By default, sequel will try to determine it by looking at the
|
83
93
|
# associated model's assocations for a association that matches
|
84
94
|
# the current association's key(s). Set to nil to not use a reciprocal.
|
85
|
-
#
|
86
|
-
#
|
87
|
-
#
|
95
|
+
# - :select - the attributes to select. Defaults to the associated class's
|
96
|
+
# table_name.*, which means it doesn't include the attributes from the
|
97
|
+
# join table in a many_to_many association. If you want to include the join table attributes, you can
|
98
|
+
# use this option, but beware that the join table attributes can clash with
|
99
|
+
# attributes from the model table, so you should alias any attributes that have
|
100
|
+
# the same name in both the join table and the associated table.
|
88
101
|
# * :many_to_one:
|
89
102
|
# - :key - foreign_key in current model's table that references
|
90
103
|
# associated model's primary key, as a symbol. Defaults to :"#{name}_id".
|
@@ -98,27 +111,21 @@ module Sequel::Model::Associations
|
|
98
111
|
# of current model and name of associated model, pluralized,
|
99
112
|
# underscored, sorted, and joined with '_'.
|
100
113
|
# - :left_key - foreign key in join table that points to current model's
|
101
|
-
# primary key, as a symbol.
|
114
|
+
# primary key, as a symbol. Defaults to :"#{self.name.underscore}_id".
|
102
115
|
# - :right_key - foreign key in join table that points to associated
|
103
|
-
# model's primary key, as a symbol.
|
104
|
-
# - :
|
105
|
-
#
|
106
|
-
# join table. If you want to include the join table attributes, you can
|
107
|
-
# use this option, but beware that the join table attributes can clash with
|
108
|
-
# attributes from the model table, so you should alias any attributes that have
|
109
|
-
# the same name in both the join table and the associated table.
|
116
|
+
# model's primary key, as a symbol. Defaults to Defaults to :"#{name.to_s.singularize}_id".
|
117
|
+
# - :graph_join_table_conditions - The conditions to use on the SQL join for the join table when eagerly loading
|
118
|
+
# the association via eager_graph
|
110
119
|
def associate(type, name, opts = {}, &block)
|
111
120
|
# check arguments
|
112
121
|
raise ArgumentError unless [:many_to_one, :one_to_many, :many_to_many].include?(type) && Symbol === name
|
113
122
|
|
114
123
|
# merge early so we don't modify opts
|
115
|
-
opts = opts.merge(:type => type, :name => name, :block => block, :cache => true)
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
opts[:class] = opts[:from]
|
121
|
-
end
|
124
|
+
opts = opts.merge(:type => type, :name => name, :block => block, :cache => true, :model => self)
|
125
|
+
opts = AssociationReflection.new.merge!(opts)
|
126
|
+
opts[:eager_block] = block unless opts.include?(:eager_block)
|
127
|
+
opts[:graph_join_type] ||= :left_outer
|
128
|
+
opts[:graph_conditions] = opts[:graph_conditions] ? opts[:graph_conditions].to_a : []
|
122
129
|
|
123
130
|
# find class
|
124
131
|
case opts[:class]
|
@@ -164,10 +171,6 @@ module Sequel::Model::Associations
|
|
164
171
|
alias_method :has_and_belongs_to_many, :many_to_many
|
165
172
|
|
166
173
|
private
|
167
|
-
# The class related to the given association reflection
|
168
|
-
def associated_class(opts)
|
169
|
-
opts[:class] ||= opts[:class_name].constantize
|
170
|
-
end
|
171
174
|
|
172
175
|
# Name symbol for add association method
|
173
176
|
def association_add_method_name(name)
|
@@ -179,7 +182,12 @@ module Sequel::Model::Associations
|
|
179
182
|
:"@#{name}"
|
180
183
|
end
|
181
184
|
|
182
|
-
# Name symbol for
|
185
|
+
# Name symbol for remove_all association method
|
186
|
+
def association_remove_all_method_name(name)
|
187
|
+
:"remove_all_#{name}"
|
188
|
+
end
|
189
|
+
|
190
|
+
# Name symbol for remove association method
|
183
191
|
def association_remove_method_name(name)
|
184
192
|
:"remove_#{name.to_s.singularize}"
|
185
193
|
end
|
@@ -190,7 +198,7 @@ module Sequel::Model::Associations
|
|
190
198
|
@association_reflections ||= {}
|
191
199
|
end
|
192
200
|
|
193
|
-
#
|
201
|
+
# Adds association methods to the model for *_to_many associations.
|
194
202
|
def def_association_dataset_methods(name, opts, &block)
|
195
203
|
dataset_method = :"#{name}_dataset"
|
196
204
|
helper_method = :"#{name}_helper"
|
@@ -199,7 +207,7 @@ module Sequel::Model::Associations
|
|
199
207
|
|
200
208
|
# define a method returning the association dataset (with optional order)
|
201
209
|
if order = opts[:order]
|
202
|
-
class_def(dataset_method) {instance_eval(&block).order(order)}
|
210
|
+
class_def(dataset_method) {instance_eval(&block).order(*order)}
|
203
211
|
else
|
204
212
|
class_def(dataset_method, &block)
|
205
213
|
end
|
@@ -225,83 +233,88 @@ module Sequel::Model::Associations
|
|
225
233
|
end
|
226
234
|
objs = ds.all
|
227
235
|
# Only one_to_many associations should set the reciprocal object
|
228
|
-
if (opts[:type] == :one_to_many) && (reciprocal =
|
236
|
+
if (opts[:type] == :one_to_many) && (reciprocal = opts.reciprocal)
|
229
237
|
objs.each{|o| o.instance_variable_set(reciprocal, self)}
|
230
238
|
end
|
231
239
|
instance_variable_set(ivar, objs)
|
232
240
|
end
|
233
241
|
end
|
234
242
|
end
|
235
|
-
|
236
|
-
# Defines an association getter method, caching the block result in an
|
237
|
-
# instance variable. The defined method takes an optional reload parameter
|
238
|
-
# that can be set to true in order to bypass the cache.
|
239
|
-
def def_association_getter(name, &block)
|
240
|
-
ivar = association_ivar(name)
|
241
|
-
class_def(name) do |*reload|
|
242
|
-
if !reload[0] && obj = instance_variable_get(ivar)
|
243
|
-
obj == :null ? nil : obj
|
244
|
-
else
|
245
|
-
obj = instance_eval(&block)
|
246
|
-
instance_variable_set(ivar, obj || :null)
|
247
|
-
obj
|
248
|
-
end
|
249
|
-
end
|
250
|
-
end
|
251
243
|
|
252
244
|
# Adds many_to_many association instance methods
|
253
245
|
def def_many_to_many(name, opts)
|
254
|
-
assoc_class = method(:associated_class) # late binding of association dataset
|
255
|
-
recip_assoc = method(:reciprocal_association) # late binding of the reciprocal association
|
256
246
|
ivar = association_ivar(name)
|
257
247
|
left = (opts[:left_key] ||= default_remote_key)
|
258
|
-
right = (opts[:right_key] ||=
|
248
|
+
right = (opts[:right_key] ||= default_foreign_key(opts))
|
259
249
|
opts[:class_name] ||= name.to_s.singularize.camelize
|
260
250
|
join_table = (opts[:join_table] ||= default_join_table_name(opts))
|
251
|
+
opts[:left_key_alias] ||= :"x_foreign_key_x"
|
252
|
+
opts[:left_key_select] ||= :"#{join_table}__#{left}___#{opts[:left_key_alias]}"
|
253
|
+
opts[:graph_join_table_conditions] = opts[:graph_join_table_conditions] ? opts[:graph_join_table_conditions].to_a : []
|
261
254
|
database = db
|
262
255
|
|
263
256
|
def_association_dataset_methods(name, opts) do
|
264
|
-
|
265
|
-
key = (opts[:right_primary_key] ||= :"#{klass.table_name}__#{klass.primary_key}")
|
266
|
-
selection = (opts[:select] ||= klass.table_name.*)
|
267
|
-
klass.select(selection).inner_join(join_table, right => key, left => pk)
|
257
|
+
opts.associated_class.select(*opts.select).inner_join(join_table, [[right, opts.associated_primary_key], [left, pk]])
|
268
258
|
end
|
269
259
|
|
270
260
|
class_def(association_add_method_name(name)) do |o|
|
271
|
-
database[join_table].insert(left
|
261
|
+
database[join_table].insert(left=>pk, right=>o.pk)
|
272
262
|
if arr = instance_variable_get(ivar)
|
273
263
|
arr.push(o)
|
274
264
|
end
|
275
|
-
if (reciprocal =
|
265
|
+
if (reciprocal = opts.reciprocal) && (list = o.instance_variable_get(reciprocal)) \
|
276
266
|
&& !(list.include?(self))
|
277
267
|
list.push(self)
|
278
268
|
end
|
279
269
|
o
|
280
270
|
end
|
281
271
|
class_def(association_remove_method_name(name)) do |o|
|
282
|
-
database[join_table].filter(left
|
272
|
+
database[join_table].filter([[left, pk], [right, o.pk]]).delete
|
283
273
|
if arr = instance_variable_get(ivar)
|
284
274
|
arr.delete(o)
|
285
275
|
end
|
286
|
-
if (reciprocal =
|
276
|
+
if (reciprocal = opts.reciprocal) && (list = o.instance_variable_get(reciprocal))
|
287
277
|
list.delete(self)
|
288
278
|
end
|
289
279
|
o
|
290
280
|
end
|
281
|
+
class_def(association_remove_all_method_name(name)) do
|
282
|
+
database[join_table].filter(left=>pk).delete
|
283
|
+
if arr = instance_variable_get(ivar)
|
284
|
+
reciprocal = opts.reciprocal
|
285
|
+
ret = arr.dup
|
286
|
+
arr.each do |o|
|
287
|
+
if reciprocal && (list = o.instance_variable_get(reciprocal))
|
288
|
+
list.delete(self)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
instance_variable_set(ivar, [])
|
293
|
+
ret
|
294
|
+
end
|
291
295
|
end
|
292
296
|
|
293
297
|
# Adds many_to_one association instance methods
|
294
298
|
def def_many_to_one(name, opts)
|
295
|
-
assoc_class = method(:associated_class) # late binding of association dataset
|
296
|
-
recip_assoc = method(:reciprocal_association) # late binding of the reciprocal association
|
297
299
|
ivar = association_ivar(name)
|
298
300
|
|
299
|
-
key = (opts[:key] ||=
|
301
|
+
key = (opts[:key] ||= default_foreign_key(opts))
|
300
302
|
opts[:class_name] ||= name.to_s.camelize
|
301
303
|
|
302
|
-
|
304
|
+
class_def(name) do |*reload|
|
305
|
+
if !reload[0] && obj = instance_variable_get(ivar)
|
306
|
+
obj == :null ? nil : obj
|
307
|
+
else
|
308
|
+
obj = if fk = send(key)
|
309
|
+
opts.associated_class.select(*opts.select).filter(opts.associated_primary_key=>fk).first
|
310
|
+
end
|
311
|
+
instance_variable_set(ivar, obj || :null)
|
312
|
+
obj
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
303
316
|
class_def(:"#{name}=") do |o|
|
304
|
-
old_val = instance_variable_get(ivar) if reciprocal =
|
317
|
+
old_val = instance_variable_get(ivar) if reciprocal = opts.reciprocal
|
305
318
|
instance_variable_set(ivar, o)
|
306
319
|
send(:"#{key}=", (o.pk if o))
|
307
320
|
if reciprocal && (old_val != o)
|
@@ -318,13 +331,11 @@ module Sequel::Model::Associations
|
|
318
331
|
|
319
332
|
# Adds one_to_many association instance methods
|
320
333
|
def def_one_to_many(name, opts)
|
321
|
-
assoc_class = method(:associated_class) # late binding of association dataset
|
322
|
-
recip_assoc = method(:reciprocal_association) # late binding of the reciprocal association
|
323
334
|
ivar = association_ivar(name)
|
324
335
|
key = (opts[:key] ||= default_remote_key)
|
325
336
|
opts[:class_name] ||= name.to_s.singularize.camelize
|
326
337
|
|
327
|
-
def_association_dataset_methods(name, opts) {
|
338
|
+
def_association_dataset_methods(name, opts) {opts.associated_class.select(*opts.select).filter(key => pk)}
|
328
339
|
|
329
340
|
class_def(association_add_method_name(name)) do |o|
|
330
341
|
o.send(:"#{key}=", pk)
|
@@ -332,7 +343,7 @@ module Sequel::Model::Associations
|
|
332
343
|
if arr = instance_variable_get(ivar)
|
333
344
|
arr.push(o)
|
334
345
|
end
|
335
|
-
if reciprocal =
|
346
|
+
if reciprocal = opts.reciprocal
|
336
347
|
o.instance_variable_set(reciprocal, self)
|
337
348
|
end
|
338
349
|
o
|
@@ -343,46 +354,40 @@ module Sequel::Model::Associations
|
|
343
354
|
if arr = instance_variable_get(ivar)
|
344
355
|
arr.delete(o)
|
345
356
|
end
|
346
|
-
if reciprocal =
|
357
|
+
if reciprocal = opts.reciprocal
|
347
358
|
o.instance_variable_set(reciprocal, :null)
|
348
359
|
end
|
349
360
|
o
|
350
361
|
end
|
362
|
+
class_def(association_remove_all_method_name(name)) do
|
363
|
+
opts.associated_class.filter(key=>pk).update(key=>nil)
|
364
|
+
if arr = instance_variable_get(ivar)
|
365
|
+
ret = arr.dup
|
366
|
+
if reciprocal = opts.reciprocal
|
367
|
+
arr.each{|o| o.instance_variable_set(reciprocal, :null)}
|
368
|
+
end
|
369
|
+
end
|
370
|
+
instance_variable_set(ivar, [])
|
371
|
+
ret
|
372
|
+
end
|
351
373
|
end
|
352
374
|
|
375
|
+
# Default foreign key name symbol for foreign key in current model's table that points to
|
376
|
+
# the given association's table's primary key.
|
377
|
+
def default_foreign_key(reflection)
|
378
|
+
name = reflection[:name]
|
379
|
+
:"#{reflection[:type] == :many_to_one ? name : name.to_s.singularize}_id"
|
380
|
+
end
|
381
|
+
|
353
382
|
# Name symbol for default join table
|
354
383
|
def default_join_table_name(opts)
|
355
384
|
([opts[:class_name].demodulize, name.demodulize]. \
|
356
385
|
map{|i| i.pluralize.underscore}.sort.join('_')).to_sym
|
357
386
|
end
|
358
387
|
|
359
|
-
#
|
388
|
+
# Default foreign key name symbol for key in associated table that points to
|
389
|
+
# current table's primary key.
|
360
390
|
def default_remote_key
|
361
391
|
:"#{name.demodulize.underscore}_id"
|
362
392
|
end
|
363
|
-
|
364
|
-
# Sets the reciprocal association variable in the reflection, if one exists
|
365
|
-
def reciprocal_association(reflection)
|
366
|
-
return reflection[:reciprocal] if reflection.include?(:reciprocal)
|
367
|
-
reciprocal_type = ::Sequel::Model::Associations::RECIPROCAL_ASSOCIATIONS[reflection[:type]]
|
368
|
-
if reciprocal_type == :many_to_many
|
369
|
-
left_key = reflection[:left_key]
|
370
|
-
right_key = reflection[:right_key]
|
371
|
-
join_table = reflection[:join_table]
|
372
|
-
associated_class(reflection).all_association_reflections.each do |assoc_reflect|
|
373
|
-
if assoc_reflect[:type] == :many_to_many && assoc_reflect[:left_key] == right_key \
|
374
|
-
&& assoc_reflect[:right_key] == left_key && assoc_reflect[:join_table] == join_table
|
375
|
-
return reflection[:reciprocal] = association_ivar(assoc_reflect[:name]).to_s.freeze
|
376
|
-
end
|
377
|
-
end
|
378
|
-
else
|
379
|
-
key = reflection[:key]
|
380
|
-
associated_class(reflection).all_association_reflections.each do |assoc_reflect|
|
381
|
-
if assoc_reflect[:type] == reciprocal_type && assoc_reflect[:key] == key
|
382
|
-
return reflection[:reciprocal] = association_ivar(assoc_reflect[:name])
|
383
|
-
end
|
384
|
-
end
|
385
|
-
end
|
386
|
-
reflection[:reciprocal] = nil
|
387
|
-
end
|
388
393
|
end
|