sequel 1.5.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|