sequel 3.11.0 → 3.12.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +70 -0
- data/Rakefile +1 -1
- data/doc/active_record.rdoc +896 -0
- data/doc/advanced_associations.rdoc +46 -31
- data/doc/association_basics.rdoc +14 -9
- data/doc/dataset_basics.rdoc +3 -3
- data/doc/migration.rdoc +1011 -0
- data/doc/model_hooks.rdoc +198 -0
- data/doc/querying.rdoc +811 -86
- data/doc/release_notes/3.12.0.txt +304 -0
- data/doc/sharding.rdoc +17 -0
- data/doc/sql.rdoc +537 -0
- data/doc/validations.rdoc +501 -0
- data/lib/sequel/adapters/jdbc.rb +19 -27
- data/lib/sequel/adapters/jdbc/postgresql.rb +0 -7
- data/lib/sequel/adapters/mysql.rb +5 -4
- data/lib/sequel/adapters/odbc.rb +3 -2
- data/lib/sequel/adapters/shared/mssql.rb +7 -6
- data/lib/sequel/adapters/shared/mysql.rb +2 -7
- data/lib/sequel/adapters/shared/postgres.rb +2 -8
- data/lib/sequel/adapters/shared/sqlite.rb +2 -5
- data/lib/sequel/adapters/sqlite.rb +4 -4
- data/lib/sequel/core.rb +0 -1
- data/lib/sequel/database.rb +2 -1060
- data/lib/sequel/database/connecting.rb +227 -0
- data/lib/sequel/database/dataset.rb +58 -0
- data/lib/sequel/database/dataset_defaults.rb +127 -0
- data/lib/sequel/database/logging.rb +62 -0
- data/lib/sequel/database/misc.rb +246 -0
- data/lib/sequel/database/query.rb +390 -0
- data/lib/sequel/database/schema_generator.rb +7 -3
- data/lib/sequel/database/schema_methods.rb +351 -7
- data/lib/sequel/dataset/actions.rb +9 -2
- data/lib/sequel/dataset/misc.rb +6 -2
- data/lib/sequel/dataset/mutation.rb +3 -11
- data/lib/sequel/dataset/query.rb +49 -6
- data/lib/sequel/exceptions.rb +3 -0
- data/lib/sequel/extensions/migration.rb +395 -113
- data/lib/sequel/extensions/schema_dumper.rb +21 -13
- data/lib/sequel/model.rb +27 -25
- data/lib/sequel/model/associations.rb +72 -34
- data/lib/sequel/model/base.rb +74 -18
- data/lib/sequel/model/errors.rb +8 -1
- data/lib/sequel/plugins/active_model.rb +8 -0
- data/lib/sequel/plugins/association_pks.rb +87 -0
- data/lib/sequel/plugins/association_proxies.rb +8 -0
- data/lib/sequel/plugins/boolean_readers.rb +12 -6
- data/lib/sequel/plugins/caching.rb +14 -7
- data/lib/sequel/plugins/class_table_inheritance.rb +15 -9
- data/lib/sequel/plugins/composition.rb +2 -1
- data/lib/sequel/plugins/force_encoding.rb +10 -7
- data/lib/sequel/plugins/hook_class_methods.rb +12 -11
- data/lib/sequel/plugins/identity_map.rb +9 -0
- data/lib/sequel/plugins/instance_hooks.rb +23 -13
- data/lib/sequel/plugins/lazy_attributes.rb +4 -1
- data/lib/sequel/plugins/many_through_many.rb +18 -4
- data/lib/sequel/plugins/nested_attributes.rb +1 -0
- data/lib/sequel/plugins/optimistic_locking.rb +1 -1
- data/lib/sequel/plugins/rcte_tree.rb +9 -8
- data/lib/sequel/plugins/schema.rb +8 -0
- data/lib/sequel/plugins/serialization.rb +1 -3
- data/lib/sequel/plugins/sharding.rb +135 -0
- data/lib/sequel/plugins/single_table_inheritance.rb +117 -25
- data/lib/sequel/plugins/skip_create_refresh.rb +35 -0
- data/lib/sequel/plugins/string_stripper.rb +26 -0
- data/lib/sequel/plugins/tactical_eager_loading.rb +8 -0
- data/lib/sequel/plugins/timestamps.rb +15 -2
- data/lib/sequel/plugins/touch.rb +13 -0
- data/lib/sequel/plugins/update_primary_key.rb +48 -0
- data/lib/sequel/plugins/validation_class_methods.rb +8 -0
- data/lib/sequel/plugins/validation_helpers.rb +1 -1
- data/lib/sequel/sql.rb +17 -20
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/postgres_spec.rb +5 -5
- data/spec/core/core_sql_spec.rb +17 -1
- data/spec/core/database_spec.rb +17 -5
- data/spec/core/dataset_spec.rb +31 -8
- data/spec/core/schema_generator_spec.rb +8 -1
- data/spec/core/schema_spec.rb +13 -0
- data/spec/extensions/association_pks_spec.rb +85 -0
- data/spec/extensions/hook_class_methods_spec.rb +9 -9
- data/spec/extensions/migration_spec.rb +339 -219
- data/spec/extensions/schema_dumper_spec.rb +28 -17
- data/spec/extensions/sharding_spec.rb +272 -0
- data/spec/extensions/single_table_inheritance_spec.rb +92 -4
- data/spec/extensions/skip_create_refresh_spec.rb +17 -0
- data/spec/extensions/string_stripper_spec.rb +23 -0
- data/spec/extensions/update_primary_key_spec.rb +65 -0
- data/spec/extensions/validation_class_methods_spec.rb +5 -5
- data/spec/files/bad_down_migration/001_create_alt_basic.rb +4 -0
- data/spec/files/bad_down_migration/002_create_alt_advanced.rb +4 -0
- data/spec/files/bad_timestamped_migrations/1273253849_create_sessions.rb +9 -0
- data/spec/files/bad_timestamped_migrations/1273253851_create_nodes.rb +9 -0
- data/spec/files/bad_timestamped_migrations/1273253853_3_create_users.rb +3 -0
- data/spec/files/bad_up_migration/001_create_alt_basic.rb +4 -0
- data/spec/files/bad_up_migration/002_create_alt_advanced.rb +3 -0
- data/spec/files/convert_to_timestamp_migrations/001_create_sessions.rb +9 -0
- data/spec/files/convert_to_timestamp_migrations/002_create_nodes.rb +9 -0
- data/spec/files/convert_to_timestamp_migrations/003_3_create_users.rb +4 -0
- data/spec/files/convert_to_timestamp_migrations/1273253850_create_artists.rb +9 -0
- data/spec/files/convert_to_timestamp_migrations/1273253852_create_albums.rb +9 -0
- data/spec/files/duplicate_integer_migrations/001_create_alt_advanced.rb +4 -0
- data/spec/files/duplicate_integer_migrations/001_create_alt_basic.rb +4 -0
- data/spec/files/duplicate_timestamped_migrations/1273253849_create_sessions.rb +9 -0
- data/spec/files/duplicate_timestamped_migrations/1273253853_create_nodes.rb +9 -0
- data/spec/files/duplicate_timestamped_migrations/1273253853_create_users.rb +4 -0
- data/spec/files/integer_migrations/001_create_sessions.rb +9 -0
- data/spec/files/integer_migrations/002_create_nodes.rb +9 -0
- data/spec/files/integer_migrations/003_3_create_users.rb +4 -0
- data/spec/files/interleaved_timestamped_migrations/1273253849_create_sessions.rb +9 -0
- data/spec/files/interleaved_timestamped_migrations/1273253850_create_artists.rb +9 -0
- data/spec/files/interleaved_timestamped_migrations/1273253851_create_nodes.rb +9 -0
- data/spec/files/interleaved_timestamped_migrations/1273253852_create_albums.rb +9 -0
- data/spec/files/interleaved_timestamped_migrations/1273253853_3_create_users.rb +4 -0
- data/spec/files/missing_integer_migrations/001_create_alt_basic.rb +4 -0
- data/spec/files/missing_integer_migrations/003_create_alt_advanced.rb +4 -0
- data/spec/files/missing_timestamped_migrations/1273253849_create_sessions.rb +9 -0
- data/spec/files/missing_timestamped_migrations/1273253853_3_create_users.rb +4 -0
- data/spec/files/timestamped_migrations/1273253849_create_sessions.rb +9 -0
- data/spec/files/timestamped_migrations/1273253851_create_nodes.rb +9 -0
- data/spec/files/timestamped_migrations/1273253853_3_create_users.rb +4 -0
- data/spec/files/uppercase_timestamped_migrations/1273253849_CREATE_SESSIONS.RB +9 -0
- data/spec/files/uppercase_timestamped_migrations/1273253851_CREATE_NODES.RB +9 -0
- data/spec/files/uppercase_timestamped_migrations/1273253853_3_CREATE_USERS.RB +4 -0
- data/spec/integration/eager_loader_test.rb +20 -20
- data/spec/integration/migrator_test.rb +187 -0
- data/spec/integration/plugin_test.rb +150 -0
- data/spec/integration/schema_test.rb +13 -2
- data/spec/model/associations_spec.rb +41 -14
- data/spec/model/base_spec.rb +69 -0
- data/spec/model/eager_loading_spec.rb +7 -3
- data/spec/model/record_spec.rb +79 -4
- data/spec/model/validations_spec.rb +21 -9
- metadata +66 -5
- data/doc/schema.rdoc +0 -36
- data/lib/sequel/database/schema_sql.rb +0 -320
@@ -0,0 +1,198 @@
|
|
1
|
+
= Model Hooks
|
2
|
+
|
3
|
+
This guide is based on http://guides.rubyonrails.org/activerecord_validations_callbacks.html
|
4
|
+
|
5
|
+
== Overview
|
6
|
+
|
7
|
+
Model hooks, also known as model callbacks, are used to specify actions that occur at a given point in a model instance's lifecycle, such as before or after the model object is saved, created, updated, destroyed, or validated.
|
8
|
+
|
9
|
+
== Basic Usage
|
10
|
+
|
11
|
+
<tt>Sequel::Model</tt> uses instance methods for hooks. To define a hook on a model, you just add an instance method to the model class:
|
12
|
+
|
13
|
+
class Album < Sequel::Model
|
14
|
+
def before_create
|
15
|
+
self.created_at ||= Time.now
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
The one important thing to note here is the call to +super+ inside the hook. Whenever you override one of Sequel::Model's methods, you should be calling +super+ to get the default behavior. Many of Sequel's built in plugins work by overriding the hook methods and calling +super+. If you use these plugins and override the hook methods but do not call +super+, it's likely the plugins will not work correctly.
|
21
|
+
|
22
|
+
== Available Hooks
|
23
|
+
|
24
|
+
Sequel calls hooks in the following order when saving/creating a new object (one that does not already exist in the database):
|
25
|
+
|
26
|
+
* +before_validation+
|
27
|
+
* +after_validation+
|
28
|
+
* +before_save+
|
29
|
+
* +before_create+
|
30
|
+
* INSERT QUERY
|
31
|
+
* +after_create+
|
32
|
+
* +after_save+
|
33
|
+
|
34
|
+
Sequel calls hooks in the following order when saving an existing object:
|
35
|
+
|
36
|
+
* +before_validation+
|
37
|
+
* +after_validation+
|
38
|
+
* +before_save+
|
39
|
+
* +before_update+
|
40
|
+
* UPDATE QUERY
|
41
|
+
* +after_update+
|
42
|
+
* +after_save+
|
43
|
+
|
44
|
+
Note that all of the hook calls are the same, except that +before_create+ and +after_create+ are used for a new object, and +before_update+ and +after_update+ are used for an existing object. Note that +before_save+ is called in both cases, before either +before_create+ or +before_update+, and that +after_save+ is also called in both cases, after either +after_create+ or +after_update+.
|
45
|
+
|
46
|
+
Also note that the validation hooks are not called if the <tt>:validate => false</tt> option is passed to save. However, the validation hooks are called if you call <tt>Model#valid?</tt> manually:
|
47
|
+
|
48
|
+
* +before_validation+
|
49
|
+
* VALIDATION HAPPENS
|
50
|
+
* +after_validation+
|
51
|
+
|
52
|
+
Sequel calls hooks in the following order when destroying an existing object:
|
53
|
+
|
54
|
+
* +before_destroy+
|
55
|
+
* DELETE QUERY
|
56
|
+
* +after_destroy+
|
57
|
+
|
58
|
+
Note that these hooks are only called when using <tt>Model#destroy</tt>, they are not called if you use <tt>Model#delete</tt>.
|
59
|
+
|
60
|
+
<tt>Sequel::Model</tt> does support one additional hook, +after_intialize+, which is called after the model object has been initalized. It can be used to set default attribute values for new objects, since by default new <tt>Sequel::Model</tt> objects have no attributes, and the attributes are not filled in until the model object is saved. You should be careful when you are using +after_initialize+, since it is called for every created record. So if you run a query that returns 1000 model objects, it will be called 1000 times.
|
61
|
+
|
62
|
+
== Running Hooks
|
63
|
+
|
64
|
+
Sequel does not provide a simple way to turn off the running of save/create/update hooks. If you attempt to save a model object, the save hooks are always called. All model instance methods that modify the database call save in some manner, so you can be sure that if you define the hooks, they will be called when you save the object.
|
65
|
+
|
66
|
+
However, you should note that there are plenty of ways to modify the database without saving a model object. One example is by using plain datasets, or one of the model's dataset methods:
|
67
|
+
|
68
|
+
Album.filter(:name=>'RF').update(:copies_sold=>:copies_sold + 1)
|
69
|
+
# UPDATE albums SET copies_sold = copies_sold + 1 WHERE name = 'RF'
|
70
|
+
|
71
|
+
In this case, the +update+ method is called on the dataset returned by <tt>Album.filter</tt>. Even if there is only a single object with the name RF, this will not call any hooks. If you want model hooks to be called, you need to make sure to operate on a model object:
|
72
|
+
|
73
|
+
album = Album.first(:name=>'RF')
|
74
|
+
album.update(:copies_sold=>album.copies_sold + 1)
|
75
|
+
# UPDATE albums SET copies_sold = 2 WHERE id = 1
|
76
|
+
|
77
|
+
For the destroy hooks, you need to make sure you call +destroy+ on the object:
|
78
|
+
|
79
|
+
album.destroy # runs destroy hooks
|
80
|
+
|
81
|
+
== Skipping Hooks
|
82
|
+
|
83
|
+
Sequel makes it easy to skip destroy hooks by calling +delete+ instead of +destroy+:
|
84
|
+
|
85
|
+
album.delete # does not run destroy hooks
|
86
|
+
|
87
|
+
However, skipping hooks is a bad idea in general and should be avoided. As mentioned above, Sequel doesn't allow you to turn off the running of save hooks. If you know what you are doing and really want to skip them, you need to drop down to the dataset level to do so. This can be done for a specific model object by using the +this+ method for a dataset that represents a single object:
|
88
|
+
|
89
|
+
album.this # dataset
|
90
|
+
|
91
|
+
The +this+ dataset works just like any other dataset, so you can call +update+ on it to modify it:
|
92
|
+
|
93
|
+
album.this.update(:copies_sold=>:copies_sold + 1)
|
94
|
+
|
95
|
+
If you want to insert a row into the model's table without running the creation hooks, you can use <tt>Model.insert</tt> instead of <tt>Model.create</tt>:
|
96
|
+
|
97
|
+
Album.insert(:name=>'RF') # does not run hooks
|
98
|
+
|
99
|
+
== Halting Hook Processing
|
100
|
+
|
101
|
+
Sequel uses a convention that if any <tt>before_*</tt> hook method returns false (but not nil), that the action will be canceled. You can use this to implement validation-like behavior, that will run even if validations are skipped. For example:
|
102
|
+
|
103
|
+
class Album < Sequel::Model
|
104
|
+
def before_save
|
105
|
+
return false if name == ''
|
106
|
+
super
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
While returning false is not really recommended, you should be aware of this behavior so that you do not inadvertently return false.
|
111
|
+
|
112
|
+
By default, Sequel runs hooks other than validation hooks inside a transaction, so if you abort the hook by returning false in a before hook or by raising an exception in the hook, Sequel will rollback the transaction. However, note that the implicit use of transactions when saving and destroying model objects is conditional (it depends on the model instance's +use_transactions+ setting).
|
113
|
+
|
114
|
+
== Conditional Hooks
|
115
|
+
|
116
|
+
Sometimes you only take to take a certain action in a hook if the object meets a certain condition. For example, let's say you only want to make sure a timestamp is set when updating if the object is at a certain status level:
|
117
|
+
|
118
|
+
class Album < Sequel::Model
|
119
|
+
def before_update
|
120
|
+
self.timestamp ||= Time.now if status_id > 3
|
121
|
+
super
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
Note how this hook action is made conditional just be using the standard ruby +if+ conditional. Sequel makes it easy to handle conditional hook actions by using standard ruby conditionals inside the instance methods.
|
126
|
+
|
127
|
+
== Using Hooks in Multiple Classes
|
128
|
+
|
129
|
+
If you want all your model classes to use the same hook, you can just define that hook in Sequel::Model:
|
130
|
+
|
131
|
+
class Sequel::Model
|
132
|
+
def before_create
|
133
|
+
self.created_at ||= Time.now
|
134
|
+
super
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
Just remember to call +super+ whenever you override the method in a subclass. Note that +super+ is also used when overriding the hook in <tt>Sequel::Model</tt> itself. This is important as if you add any plugins to Sequel::Model itself, if you override a hook in <tt>Sequel::Model</tt> and do not call +super+, the plugin may not work correctly.
|
139
|
+
|
140
|
+
If you don't want all classes to use the same hook, but want to reuse hooks in multiple classes, you should use a plugin or a simple module:
|
141
|
+
|
142
|
+
=== Plugin
|
143
|
+
|
144
|
+
module SetCreatedAt
|
145
|
+
module InstanceMethods
|
146
|
+
def before_create
|
147
|
+
self.created_at ||= Time.now
|
148
|
+
super
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
Album.plugin(SetCreatedAt)
|
153
|
+
Artist.plugin(SetCreatedAt)
|
154
|
+
|
155
|
+
=== Simple Module
|
156
|
+
|
157
|
+
module SetCreatedAt
|
158
|
+
def before_create
|
159
|
+
self.created_at ||= Time.now
|
160
|
+
super
|
161
|
+
end
|
162
|
+
end
|
163
|
+
Album.send(:include, SetCreatedAt)
|
164
|
+
Artist.send(:include, SetCreatedAt)
|
165
|
+
|
166
|
+
== +super+ Ordering
|
167
|
+
|
168
|
+
While it's not enforced anywhere, it's a good idea to make +super+ the last expression when you override a before hook, and the first expression when you override an after hook:
|
169
|
+
|
170
|
+
class Album < Sequel::Model
|
171
|
+
def before_save
|
172
|
+
self.updated_at ||= Time.now
|
173
|
+
super
|
174
|
+
end
|
175
|
+
|
176
|
+
def after_save
|
177
|
+
super
|
178
|
+
AuditLog.create(:log=>"Album #{name} created")
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
This allows the following general principles to be true:
|
183
|
+
|
184
|
+
* before hooks are run in reverse order of inclusion
|
185
|
+
* after hooks are run in order of inclusion
|
186
|
+
|
187
|
+
So if you define the same before hook in both a model and a plugin that the model uses, the hooks will be called in this order:
|
188
|
+
|
189
|
+
* model before hook
|
190
|
+
* plugin before hook
|
191
|
+
* plugin after hook
|
192
|
+
* model after hook
|
193
|
+
|
194
|
+
Again, Sequel does not enforce that, and you are free to call +super+ in an order other than the recommended one (just make sure that you call it).
|
195
|
+
|
196
|
+
== +hook_class_methods+
|
197
|
+
|
198
|
+
While it's recommended to write your hooks as instance methods, Sequel ships with a +hook_class_methods+ plugin that allows you to define hooks via class methods. It exists mostly for legacy compatibility, but is still supported.
|
data/doc/querying.rdoc
CHANGED
@@ -12,80 +12,6 @@ While you can easily use raw SQL with Sequel, a large part of the
|
|
12
12
|
advantage you get from using Sequel is Sequel's ability to abstract
|
13
13
|
SQL from you and give you a much nicer interface.
|
14
14
|
|
15
|
-
== Setup
|
16
|
-
|
17
|
-
Some examples in this guide assume you will be using Sequel::Model
|
18
|
-
for modeling, but most should work with plain datasets. The examples
|
19
|
-
specific to Sequel::Model will have [Sequel::Model] in the heading.
|
20
|
-
|
21
|
-
Many of the examples in this guide will refer to the following model
|
22
|
-
classes:
|
23
|
-
|
24
|
-
# All classes use :id as the primary key column
|
25
|
-
|
26
|
-
class Artist < Sequel::Model
|
27
|
-
one_to_many :albums
|
28
|
-
one_to_one :address
|
29
|
-
end
|
30
|
-
|
31
|
-
class Album < Sequel::Model
|
32
|
-
many_to_one :artist
|
33
|
-
one_to_many :tracks
|
34
|
-
many_to_many :tags
|
35
|
-
end
|
36
|
-
|
37
|
-
class Address < Sequel::Model
|
38
|
-
many_to_one :artist
|
39
|
-
end
|
40
|
-
|
41
|
-
class Tag < Sequel::Model
|
42
|
-
many_to_many :albums
|
43
|
-
end
|
44
|
-
|
45
|
-
class Track < Sequel::Model
|
46
|
-
many_to_one :album
|
47
|
-
end
|
48
|
-
|
49
|
-
If you want to play with the code examples, here's some Sequel code
|
50
|
-
that will set up the database structure for you:
|
51
|
-
|
52
|
-
DB.create_table(:artists) do
|
53
|
-
primary_key :id
|
54
|
-
String :name
|
55
|
-
end
|
56
|
-
|
57
|
-
DB.create_table(:albums) do
|
58
|
-
primary_key :id
|
59
|
-
foreign_key :artist_id, :artists
|
60
|
-
String :name
|
61
|
-
end
|
62
|
-
|
63
|
-
DB.create_table(:addresses) do
|
64
|
-
primary_key :id
|
65
|
-
foreign_key :artist_id, :artists, :unique=>true
|
66
|
-
String :street
|
67
|
-
String :city
|
68
|
-
String :state
|
69
|
-
String :zip
|
70
|
-
end
|
71
|
-
|
72
|
-
DB.create_table(:tags) do
|
73
|
-
primary_key :id
|
74
|
-
String :tag
|
75
|
-
end
|
76
|
-
|
77
|
-
DB.create_table(:albums_tags) do
|
78
|
-
foreign_key :album_id, :albums
|
79
|
-
foreign_key :tag_id, :tags
|
80
|
-
end
|
81
|
-
|
82
|
-
DB.create_table(:tracks) do
|
83
|
-
primary_key :id
|
84
|
-
foreign_key :album_id, :albums
|
85
|
-
Integer :number
|
86
|
-
String :name
|
87
|
-
end
|
88
|
-
|
89
15
|
== Retrieving Objects
|
90
16
|
|
91
17
|
Sequel provides a few separate methods for retrieving objects from the
|
@@ -111,7 +37,7 @@ by its primary key value:
|
|
111
37
|
|
112
38
|
# Find artist with primary key (id) 1
|
113
39
|
artist = Artist[1]
|
114
|
-
#
|
40
|
+
# SELECT * FROM artists WHERE id = 1
|
115
41
|
=> #<Artist @values={:name=>"YJM", :id=>1}>
|
116
42
|
|
117
43
|
If there is no record with the given primary key, nil will be returned.
|
@@ -122,24 +48,24 @@ If you just want the first record in the dataset,
|
|
122
48
|
<tt>Sequel::Dataset#first</tt> is probably the most obvious method to use:
|
123
49
|
|
124
50
|
artist = Artist.first
|
125
|
-
#
|
51
|
+
# SELECT * FROM artists LIMIT 1
|
126
52
|
=> #<Artist @values={:name=>"YJM", :id=>1}>
|
127
53
|
|
128
54
|
Any options you pass to +first+ will be used as a filter:
|
129
55
|
|
130
56
|
artist = Artist.first(:name => 'YJM')
|
131
|
-
#
|
57
|
+
# SELECT * FROM artists WHERE (name = 'YJM') LIMIT 1
|
132
58
|
=> #<Artist @values={:name=>"YJM", :id=>1}>
|
133
59
|
|
134
60
|
artist = Artist.first(:name.like('Y%'))
|
135
|
-
#
|
61
|
+
# SELECT * FROM artists WHERE (name LIKE 'Y%') LIMIT 1
|
136
62
|
=> #<Artist @values={:name=>"YJM", :id=>1}>
|
137
63
|
|
138
64
|
<tt>Sequel::Dataset#[]</tt> is basically an alias for +first+, except it
|
139
65
|
requires an argument:
|
140
66
|
|
141
67
|
DB[:artists][:name => 'YJM']
|
142
|
-
#
|
68
|
+
# SELECT * FROM artists WHERE (name = 'YJM') LIMIT 1
|
143
69
|
=> {:name=>"YJM", :id=>1}
|
144
70
|
|
145
71
|
Note that while Model.[] allows you to pass a primary key directly,
|
@@ -153,13 +79,13 @@ that last requires that the dataset be ordered. Without an order, any
|
|
153
79
|
object can be considered the first as well as the last.
|
154
80
|
|
155
81
|
artist = Artist.order(:name).last
|
156
|
-
#
|
82
|
+
# SELECT * FROM artists ORDER BY name DESC LIMIT 1
|
157
83
|
=> #<Artist @values={:name=>"YJM", :id=>1}>
|
158
84
|
|
159
85
|
Note that all +last+ does is reverse the order of the dataset and then
|
160
86
|
call +first+. This is why +last+ raises a Sequel::Error if there is no
|
161
87
|
order on the dataset, because otherwise it would provide the same record
|
162
|
-
as +first+, and most users would
|
88
|
+
as +first+, and most users would find that confusing.
|
163
89
|
|
164
90
|
Note that +last+ is not necessarily going to give you the last record
|
165
91
|
in the dataset unless you give the dataset an unambiguous order.
|
@@ -171,7 +97,7 @@ a specific column. For this <tt>Sequel::Dataset#get</tt> is the method
|
|
171
97
|
you want:
|
172
98
|
|
173
99
|
artist_name = Artist.get(:name)
|
174
|
-
#
|
100
|
+
# SELECT name FROM artists LIMIT 1
|
175
101
|
=> "YJM"
|
176
102
|
|
177
103
|
=== Retrieving Multiple Objects
|
@@ -183,7 +109,7 @@ dataset, in which case <tt>Sequel::Dataset#all</tt> is the method you
|
|
183
109
|
want to use:
|
184
110
|
|
185
111
|
artists = Artist.all
|
186
|
-
#
|
112
|
+
# SELECT * FROM artists
|
187
113
|
=> [#<Artist @values={:name=>"YJM", :id=>1}>,
|
188
114
|
#<Artist @values={:name=>"AS", :id=>2}>]
|
189
115
|
|
@@ -194,7 +120,7 @@ method named each that yields hashes or model objects as they are retrieved
|
|
194
120
|
from the database:
|
195
121
|
|
196
122
|
Artist.each{|x| p x.name}
|
197
|
-
#
|
123
|
+
# SELECT * FROM artists
|
198
124
|
"YJM"
|
199
125
|
"AS"
|
200
126
|
|
@@ -202,9 +128,808 @@ This means that all of the methods in the Enumerable module are available,
|
|
202
128
|
such as +map+:
|
203
129
|
|
204
130
|
artist_names = Artist.map{|x| x.name}
|
205
|
-
#
|
131
|
+
# SELECT * FROM artists
|
206
132
|
=> ["YJM", "AS"]
|
207
133
|
|
208
134
|
==== As an Array of Column Values
|
209
135
|
|
210
|
-
|
136
|
+
Sequel also has an extended +map+ method that takes an argument. If you
|
137
|
+
provide an argument to +map+, it will return an array of values for the
|
138
|
+
given column. So the previous example can be handled more easily with:
|
139
|
+
|
140
|
+
artist_names = Artist.map(:name)
|
141
|
+
# SELECT * FROM artists
|
142
|
+
=> ["YJM", "AS"]
|
143
|
+
|
144
|
+
One difference between these two ways of return an array of values is
|
145
|
+
that providing +map+ with an argument is really doing:
|
146
|
+
|
147
|
+
artist_names = Artist.map{|x| x[:name]} # not x.name
|
148
|
+
|
149
|
+
Note that regardless of whether you provide +map+ with an argument, it
|
150
|
+
does not modify the columns selected. If you only want to select a
|
151
|
+
single column and return an array of the columns values, you can use
|
152
|
+
+select_map+:
|
153
|
+
|
154
|
+
artist_names = Artist.select_map(:name)
|
155
|
+
# SELECT name FROM artists
|
156
|
+
=> ["YJM", "AS"]
|
157
|
+
|
158
|
+
It's also common to want to order such a map, so Sequel provides a
|
159
|
+
+select_order_map+ method as well:
|
160
|
+
|
161
|
+
artist_names = Artist.select_order_map(:name)
|
162
|
+
# SELECT name FROM artists ORDER BY name
|
163
|
+
=> ["AS", "YJM"]
|
164
|
+
|
165
|
+
==== As a Hash
|
166
|
+
|
167
|
+
Sequel makes it easy to take an SQL query and return it as a ruby hash,
|
168
|
+
using the +to_hash+ method:
|
169
|
+
|
170
|
+
artist_names = Artist.to_hash(:name, :id)
|
171
|
+
# SELECT * FROM artists
|
172
|
+
=> {"YJM"=>1, "AS"=>2}
|
173
|
+
|
174
|
+
As you can see, the +to_hash+ method uses the first symbol as the key
|
175
|
+
and the second symbol as the value. So if you swap the two arguments:
|
176
|
+
|
177
|
+
artist_names = Artist.to_hash(:id, :name)
|
178
|
+
# SELECT * FROM artists
|
179
|
+
=> {1=>"YJM", 2=>"AS"}
|
180
|
+
|
181
|
+
If you only provide one argument to +to_hash+, it uses the entire hash
|
182
|
+
or model object as the value:
|
183
|
+
|
184
|
+
artist_names = DB[:artists].to_hash(:name)
|
185
|
+
# SELECT * FROM artists
|
186
|
+
=> {"YJM"=>{:id=>1, :name=>"YJM"}, "AS"=>{:id=>2, :name=>"AS"}}
|
187
|
+
|
188
|
+
Model datasets have a +to_hash+ method that can be called without any
|
189
|
+
arguments, in which case it will use the primary key as the key and
|
190
|
+
the model object as the value. This can be used to easily create an
|
191
|
+
identity map:
|
192
|
+
|
193
|
+
artist_names = Artist.to_hash
|
194
|
+
# SELECT * FROM artists
|
195
|
+
=> {1=>#<Artist @values={:id=>1, :name=>"YGM"}>,
|
196
|
+
2=>#<Artist @values={:id=>2, :name=>"AS"}>}
|
197
|
+
|
198
|
+
Note that +to_hash+ never modifies the columns selected. However, just
|
199
|
+
like Sequel has a +select_map+ method to modify the columns selected and
|
200
|
+
return an array, Sequel also has a +select_hash+ method to modify the
|
201
|
+
columns selected and return a hash:
|
202
|
+
|
203
|
+
artist_names = Artist.select_hash(:name, :id)
|
204
|
+
# SELECT name, id FROM artists
|
205
|
+
=> {"YJM"=>1, "AS"=>2}
|
206
|
+
|
207
|
+
== Modifying datasets
|
208
|
+
|
209
|
+
Note that the retrieval methods discussed above just return
|
210
|
+
the row(s) included in the existing dataset. In most cases,
|
211
|
+
you aren't interested in every row in a table, but in a subset
|
212
|
+
of the rows, based on some criteria. In Sequel, filtering
|
213
|
+
the dataset is generally done separately than retrieving
|
214
|
+
the records.
|
215
|
+
|
216
|
+
There are really two types of dataset methods that you will
|
217
|
+
be using:
|
218
|
+
|
219
|
+
1. Methods that return row(s), discussed above
|
220
|
+
2. Methods that return modified datasets, discussed below
|
221
|
+
|
222
|
+
Sequel uses a method chaining, functional style API to
|
223
|
+
modify datasets. Let's start with a simple example.
|
224
|
+
|
225
|
+
This is a basic dataset that includes all records in the
|
226
|
+
table +artists+:
|
227
|
+
|
228
|
+
ds1 = DB[:artists]
|
229
|
+
# SELECT * FROM artists
|
230
|
+
|
231
|
+
Let's say we are only interested in the artists whose names
|
232
|
+
start with "A":
|
233
|
+
|
234
|
+
ds2 = ds1.where(:name.like('A%'))
|
235
|
+
# SELECT * FROM artists WHERE name LIKE 'A%'
|
236
|
+
|
237
|
+
Here we see that +where+ returns a dataset that adds a +WHERE+
|
238
|
+
clause to the query. It's important to note that +where+ does
|
239
|
+
not modify the receiver:
|
240
|
+
|
241
|
+
ds1
|
242
|
+
# SELECT * FROM artists
|
243
|
+
ds2
|
244
|
+
# SELECT * FROM artists WHERE name LIKE 'A%'
|
245
|
+
|
246
|
+
In Sequel, most dataset methods that you will be using will
|
247
|
+
not modify the dataset itself, so you can freely use the dataset in multiple
|
248
|
+
places without worrying that its usage in one place will affect its usage
|
249
|
+
in another place. This is what is meant by a functional style API.
|
250
|
+
|
251
|
+
Let's say we only want to select the id and name columns, and that
|
252
|
+
we want to order by name:
|
253
|
+
|
254
|
+
ds3 = ds.order(:name).select(:id, :name)
|
255
|
+
# SELECT id, name FROM artists WHERE name LIKE 'A%' ORDER BY name
|
256
|
+
|
257
|
+
Note how you don't need to assign the returned value of order to a variable,
|
258
|
+
and then call select on that. Because order just returns a dataset, you can
|
259
|
+
call select directly on the returned dataset. This is what is meant by a
|
260
|
+
method chaining API.
|
261
|
+
|
262
|
+
Also note how you can call methods that modify different clauses in any order.
|
263
|
+
In this case, the WHERE clause was added first, then the ORDER clause, then the
|
264
|
+
SELECT clause was modified. This makes for a flexible API, where you can modify
|
265
|
+
any part of the query at any time.
|
266
|
+
|
267
|
+
== Filters
|
268
|
+
|
269
|
+
Filtering is probably the most common dataset modifying action done in Sequel.
|
270
|
+
Both the +where+ and +filter+ methods filter the dataset by modifying the
|
271
|
+
dataset's WHERE clause. While not quite aliases of each other, they operate
|
272
|
+
the same in most cases. Both accept a wide variety of input formats, discussed
|
273
|
+
below.
|
274
|
+
|
275
|
+
=== Hashes
|
276
|
+
|
277
|
+
The most common format for providing filters is via a hash. In general, Sequel
|
278
|
+
treats conditions specified with a hash as equality or inclusion. What type
|
279
|
+
of condition is used depends on the values in the hash.
|
280
|
+
|
281
|
+
Unless Sequel has special support for the value's class, it uses a simple
|
282
|
+
equality statement:
|
283
|
+
|
284
|
+
Artist.filter(:id=>1)
|
285
|
+
# SELECT * FROM artists WHERE id = 1
|
286
|
+
|
287
|
+
Artist.filter(:name=>'YJM')
|
288
|
+
# SELECT * FROM artists WHERE name = 'YJM'
|
289
|
+
|
290
|
+
For arrays, Sequel uses the IN operator.
|
291
|
+
|
292
|
+
Artist.filter(:id=>[1, 2])
|
293
|
+
# SELECT * FROM artists WHERE id IN (1, 2)
|
294
|
+
|
295
|
+
For datasets, Sequel uses the IN operator with a subselect:
|
296
|
+
|
297
|
+
Artist.filter(:id=>Album.select(:artist_id))
|
298
|
+
# SELECT * FROM artists WHERE id IN (
|
299
|
+
# SELECT artist_id FROM albums)
|
300
|
+
|
301
|
+
For boolean values such as nil, true, and false, Sequel uses the IS operator:
|
302
|
+
|
303
|
+
Artist.filter(:id=>nil)
|
304
|
+
# SELECT * FROM artists WHERE id IS NULL
|
305
|
+
|
306
|
+
For ranges, Sequel uses a pair of inequality statements:
|
307
|
+
|
308
|
+
Artist.filter(:id=>1..5)
|
309
|
+
# SELECT * FROM artists WHERE id >= 1 AND id <= 5
|
310
|
+
|
311
|
+
Finally, for regexps, Sequel uses an SQL regular expression. Note that this
|
312
|
+
is probably only supported on PostgreSQL and MySQL.
|
313
|
+
|
314
|
+
Artist.filter(:name=>/JM$/)
|
315
|
+
# SELECT * FROM artists WHERE name ~ 'JM$'
|
316
|
+
|
317
|
+
If there are multiple arguments in the hash, the filters are ANDed together:
|
318
|
+
|
319
|
+
Artist.filter(:id=>1, :name=>/JM$/)
|
320
|
+
# SELECT * FROM artists WHERE id = 1 AND name ~ 'JM$'
|
321
|
+
|
322
|
+
This works the same as if you used two separate filter calls:
|
323
|
+
|
324
|
+
Artist.filter(:id=>1).filter(:name=>/JM$/)
|
325
|
+
# SELECT * FROM artists WHERE id = 1 AND name ~ 'JM$'
|
326
|
+
|
327
|
+
=== Array of Two Element Arrays
|
328
|
+
|
329
|
+
If you use an array of two element arrays, it is treated as a hash. The only
|
330
|
+
advantage to using an array of two element arrays is that it allows you to
|
331
|
+
duplicate keys, so you can do:
|
332
|
+
|
333
|
+
Artist.filter([[:name, /JM$/], [:name, /^YJ/]])
|
334
|
+
# SELECT * FROM artists WHERE name ~ 'JM$' AND name ~ '^YJ'
|
335
|
+
|
336
|
+
=== Virtual Row Blocks
|
337
|
+
|
338
|
+
If a block is passed to filter, it is treated as a virtual row block:
|
339
|
+
|
340
|
+
Artist.filter{id > 5}
|
341
|
+
# SELECT * FROM artists WHERE id > 5
|
342
|
+
|
343
|
+
You can learn more about virtual row blocks in the {"Virtual Rows" guide}[link:files/doc/virtual_rows_rdoc.html].
|
344
|
+
|
345
|
+
You can provide both regular arguments and a block, in which case the results
|
346
|
+
will be ANDed together:
|
347
|
+
|
348
|
+
Artist.filter(:name=>'A'...'M'){id > 5}
|
349
|
+
# SELECT * FROM artists WHERE name >= 'A' AND name < 'M' AND id > 5
|
350
|
+
|
351
|
+
=== Symbols
|
352
|
+
|
353
|
+
If you have a boolean column in the database, and you want only true
|
354
|
+
values, you can just provide the column symbol to filter:
|
355
|
+
|
356
|
+
Artist.where(:retired)
|
357
|
+
# SELECT * FROM artists WHERE retired
|
358
|
+
|
359
|
+
=== SQL::Expression
|
360
|
+
|
361
|
+
Sequel has a DSL that allows easily creating SQL expressions. These SQL
|
362
|
+
expressions are instances of subclasses of Sequel::SQL::Expression. You've
|
363
|
+
already seen an example earlier:
|
364
|
+
|
365
|
+
Artist.filter(:name.like('Y%'))
|
366
|
+
# SELECT * FROM artists WHERE name LIKE 'Y%'
|
367
|
+
|
368
|
+
In this case Symbol#like returns a Sequel::SQL::BooleanExpression object,
|
369
|
+
which is used directly in the filter.
|
370
|
+
|
371
|
+
You can use the DSL to create arbitrarily complex expressions. SQL::Expression
|
372
|
+
objects support the & operator for +AND+, the | operator for +OR+, and the ~ operator
|
373
|
+
for inversion:
|
374
|
+
|
375
|
+
Artist.filter(:name.like('Y%') & ({:b=>1} | ~{:c=>3}))
|
376
|
+
# SELECT * FROM artists WHERE name LIKE 'Y%' AND (b = 1 OR c != 3)
|
377
|
+
|
378
|
+
You can combine these expression operators with the virtual row support:
|
379
|
+
|
380
|
+
Artist.filter{(a > 1) & ~((b(c) < 1) | d)}
|
381
|
+
# SELECT * FROM artists WHERE a > 1 AND b(c) >= 1 AND NOT d
|
382
|
+
|
383
|
+
Note the use of parentheses when using the & and | operators, as they have lower
|
384
|
+
precedence than other operators. The following will not work:
|
385
|
+
|
386
|
+
Artist.filter{a > 1 & ~(b(c) < 1 | d)}
|
387
|
+
# Raises a TypeError, as it calls Integer#| with a Sequel::SQL::Identifier
|
388
|
+
|
389
|
+
=== Strings with Placeholders
|
390
|
+
|
391
|
+
Assuming you want to get your hands dirty and write some SQL, Sequel allows you
|
392
|
+
to use strings using placeholders for the values:
|
393
|
+
|
394
|
+
Artist.filter("name LIKE ?", 'Y%')
|
395
|
+
# SELECT * FROM artists WHERE name LIKE 'Y%'
|
396
|
+
|
397
|
+
This is the most common type of placeholder, where each question mark is substituted
|
398
|
+
with the next argument:
|
399
|
+
|
400
|
+
Artist.filter("name LIKE ? AND id = ?", 'Y%', 5)
|
401
|
+
# SELECT * FROM artists WHERE name LIKE 'Y%' AND id = 5
|
402
|
+
|
403
|
+
You can also use named placeholders with a hash, where the named placeholders use
|
404
|
+
colons before the placeholder names:
|
405
|
+
|
406
|
+
Artist.filter("name LIKE :name AND id = :id", :name=>'Y%', :id=>5)
|
407
|
+
# SELECT * FROM artists WHERE name LIKE 'Y%' AND id = 5
|
408
|
+
|
409
|
+
You don't have to provide any placeholders if you don't want to:
|
410
|
+
|
411
|
+
Artist.filter("id = 2")
|
412
|
+
# SELECT * FROM artists WHERE id = 2
|
413
|
+
|
414
|
+
However, if you are using any untrusted input, you should definitely be using placeholders.
|
415
|
+
In general, unless you are hardcoding values in the strings, you should use placeholders.
|
416
|
+
You should never pass a string that has been built using interpolation, unless you are
|
417
|
+
sure of what you are doing.
|
418
|
+
|
419
|
+
Artist.filter("id = #{params[:id]}") # Don't do this!
|
420
|
+
Artist.filter("id = ?", params[:id]) # Do this instead
|
421
|
+
Artist.filter(:id=>params[:id]) # Even better
|
422
|
+
|
423
|
+
=== Inverting
|
424
|
+
|
425
|
+
You may be wondering how to specify a not equals condition in Sequel, or the NOT IN
|
426
|
+
operator. Sequel has generic support for inverting conditions, so to write a not
|
427
|
+
equals condition, you write an equals condition, and invert it:
|
428
|
+
|
429
|
+
Artist.filter(:id=>5).invert
|
430
|
+
# SELECT * FROM artists WHERE id != 5
|
431
|
+
|
432
|
+
Note that +invert+ inverts the entire filter:
|
433
|
+
|
434
|
+
Artist.filter(:id=>5).filter{name > 'A'}.invert
|
435
|
+
# SELECT * FROM artists WHERE id != 5 OR name <= 'A'
|
436
|
+
|
437
|
+
In general, +invert+ is used rarely, since +exclude+ allows you to invert only specific
|
438
|
+
filters:
|
439
|
+
|
440
|
+
Artist.exclude(:id=>5)
|
441
|
+
# SELECT * FROM artists WHERE id != 5
|
442
|
+
|
443
|
+
Artist.filter(:id=>5).exclude{name > 'A'}
|
444
|
+
# SELECT * FROM artists WHERE id = 5 OR name <= 'A'
|
445
|
+
|
446
|
+
So to do a NOT IN with an array:
|
447
|
+
|
448
|
+
Artist.exclude(:id=>[1, 2])
|
449
|
+
# SELECT * FROM artists WHERE id NOT IN (1, 2)
|
450
|
+
|
451
|
+
Or to use the NOT LIKE operator:
|
452
|
+
|
453
|
+
Artist.exclude(:name.like('%J%'))
|
454
|
+
# SELECT * FROM artists WHERE name NOT LIKE '%J%'
|
455
|
+
|
456
|
+
=== Removing
|
457
|
+
|
458
|
+
To remove all existing filters, use +unfiltered+:
|
459
|
+
|
460
|
+
Artist.filter(:id=>1).unfiltered
|
461
|
+
# SELECT * FROM artists
|
462
|
+
|
463
|
+
== Ordering
|
464
|
+
|
465
|
+
Sequel offers quite a few methods to manipulate the SQL ORDER BY clause. The
|
466
|
+
most basic of these is +order+:
|
467
|
+
|
468
|
+
Artist.order(:id)
|
469
|
+
# SELECT * FROM artists ORDER BY id
|
470
|
+
|
471
|
+
You can specify multiple arguments to order by more than one column:
|
472
|
+
|
473
|
+
Album.order(:artist_id, :id)
|
474
|
+
# SELECT * FROM album ORDER BY artist_id, id
|
475
|
+
|
476
|
+
Note that unlike +filter+, +order+ replaces an existing order, it does not
|
477
|
+
append to an existing order:
|
478
|
+
|
479
|
+
Artist.order(:id).order(:name)
|
480
|
+
# SELECT * FROM artists ORDER BY name
|
481
|
+
|
482
|
+
If you want to add a column to the end of the existing order:
|
483
|
+
|
484
|
+
Artist.order(:id).order_append(:name)
|
485
|
+
# SELECT * FROM artists ORDER BY id, name
|
486
|
+
|
487
|
+
If you want to add a column to the beginning of the existing order:
|
488
|
+
|
489
|
+
Artist.order(:id).order_prepend(:name)
|
490
|
+
# SELECT * FROM artists ORDER BY name, id
|
491
|
+
|
492
|
+
=== Reversing
|
493
|
+
|
494
|
+
Just like you can invert an existing filter, you can reverse an existing
|
495
|
+
order, using +reverse+:
|
496
|
+
|
497
|
+
Artist.order(:id).reverse
|
498
|
+
# SELECT FROM artists ORDER BY id DESC
|
499
|
+
|
500
|
+
As you might expect, +reverse+ is not used all that much. In general,
|
501
|
+
<tt>Symbol#desc</tt> is used more commonly to specify a descending order
|
502
|
+
for columns:
|
503
|
+
|
504
|
+
Artist.order(:id.desc)
|
505
|
+
# SELECT FROM artists ORDER BY id DESC
|
506
|
+
|
507
|
+
This allows you to easily use both ascending and descending orders:
|
508
|
+
|
509
|
+
Artist.order(:name, :id.desc)
|
510
|
+
# SELECT FROM artists ORDER BY name, id DESC
|
511
|
+
|
512
|
+
=== Removing
|
513
|
+
|
514
|
+
Just like you can remove filters with +unfiltered+, you can remove
|
515
|
+
orders with +unordered+:
|
516
|
+
|
517
|
+
Artist.order(:name).unordered
|
518
|
+
# SELECT * FROM artists
|
519
|
+
|
520
|
+
== Selected Columns
|
521
|
+
|
522
|
+
Sequel offers a few methods to manipulate the columns selected. As
|
523
|
+
you may be able to guess, the main method used is +select+:
|
524
|
+
|
525
|
+
Artist.select(:id, :name)
|
526
|
+
# SELECT id, name FROM artists
|
527
|
+
|
528
|
+
You just specify all of the columns that you are selecting as
|
529
|
+
arguments to the method.
|
530
|
+
|
531
|
+
If you are dealing with model objects, you'll want to include the
|
532
|
+
primary key if you want to update or destroy the object. You'll
|
533
|
+
also want to include any keys (primary or foreign) related to
|
534
|
+
associations you plan to use.
|
535
|
+
|
536
|
+
If a column is not selected, and you attempt to access it, you will
|
537
|
+
get nil:
|
538
|
+
|
539
|
+
artist = Artist.select(:name).first
|
540
|
+
# SELECT name FROM artists LIMIT 1
|
541
|
+
|
542
|
+
artist[:id]
|
543
|
+
# => nil
|
544
|
+
|
545
|
+
Like +order+, +select+ replaces the existing selected columns:
|
546
|
+
|
547
|
+
Artist.select(:id).select(:name)
|
548
|
+
# SELECT name FROM artists
|
549
|
+
|
550
|
+
To add to the existing selected columns, use +select_append+:
|
551
|
+
|
552
|
+
Artist.select(:id).select_append(:name)
|
553
|
+
# SELECT id, name FROM artists
|
554
|
+
|
555
|
+
To remove specifically selected columns, and default back to all
|
556
|
+
columns, use +select_all+:
|
557
|
+
|
558
|
+
Artist.select(:id).select_all
|
559
|
+
# SELECT * FROM artists
|
560
|
+
|
561
|
+
=== Distinct
|
562
|
+
|
563
|
+
To treat duplicate rows as a single row when retrieving the records,
|
564
|
+
use +distinct+:
|
565
|
+
|
566
|
+
Artist.distinct.select(:name)
|
567
|
+
# SELECT DISTINCT name FROM artists
|
568
|
+
|
569
|
+
Note that DISTINCT is a separate SQL clause, it's not a function
|
570
|
+
that you pass to select.
|
571
|
+
|
572
|
+
== Limit and Offset
|
573
|
+
|
574
|
+
You can limit the dataset to a given number of rows using +limit+:
|
575
|
+
|
576
|
+
Artist.limit(5)
|
577
|
+
# SELECT * FROM artists LIMIT 5
|
578
|
+
|
579
|
+
You can provide a second argument to +limit+ to specify an offset:
|
580
|
+
|
581
|
+
Artist.limit(5, 10)
|
582
|
+
# SELECT * FROM artists LIMIT 5 OFFSET 10
|
583
|
+
|
584
|
+
This would return the 11th through 15th records in the original
|
585
|
+
dataset.
|
586
|
+
|
587
|
+
To remove a limit from a dataset, use +unlimited+:
|
588
|
+
|
589
|
+
Artist.limit(5, 10).unlimited
|
590
|
+
# SELECT * FROM artists
|
591
|
+
|
592
|
+
== Grouping
|
593
|
+
|
594
|
+
The SQL GROUP BY clause is used to combine multiple rows based on
|
595
|
+
the values of a given group of columns.
|
596
|
+
|
597
|
+
To modify the GROUP BY clause of the SQL statement, you use +group+:
|
598
|
+
|
599
|
+
Album.group(:artist_id)
|
600
|
+
# SELECT * FROM albums GROUP BY artist_id
|
601
|
+
|
602
|
+
You can remove an existing grouping using +ungrouped+:
|
603
|
+
|
604
|
+
Album.group(:artist_id).ungrouped
|
605
|
+
# SELECT * FROM albums
|
606
|
+
|
607
|
+
A common use of grouping is to count based on the number of grouped rows,
|
608
|
+
and Sequel provides a +group_and_count+ method to make this easier:
|
609
|
+
|
610
|
+
Album.group_and_count(:artist_id)
|
611
|
+
# SELECT artist_id, COUNT(*) AS count FROM albums GROUP BY artist_id
|
612
|
+
|
613
|
+
This will return the number of albums for each artist_id.
|
614
|
+
|
615
|
+
== Having
|
616
|
+
|
617
|
+
The SQL HAVING clause is similar to the WHERE clause, except that
|
618
|
+
filters the results after the grouping has been applied, instead of
|
619
|
+
before. One possible use is if you only wanted to return artists
|
620
|
+
who had at least 10 albums:
|
621
|
+
|
622
|
+
Album.group_and_count(:artist_id).having{count >= 10}
|
623
|
+
# SELECT artist_id, COUNT(*) AS count FROM albums
|
624
|
+
# GROUP BY artist_id HAVING count >= 10
|
625
|
+
|
626
|
+
If you have an existing HAVING clause on your dataset, then +filter+
|
627
|
+
will add to the HAVING clause instead of the WHERE clause:
|
628
|
+
|
629
|
+
Album.group_and_count(:artist_id).
|
630
|
+
having{count >= 10}.filter{count < 15}
|
631
|
+
# SELECT artist_id, COUNT(*) AS count FROM albums
|
632
|
+
# GROUP BY artist_id HAVING count >= 10 AND count < 15
|
633
|
+
|
634
|
+
Unlike +filter+, +where+ always affects the WHERE clause:
|
635
|
+
|
636
|
+
Album.group_and_count(:artist_id).
|
637
|
+
having{count >= 10}.where(:name.like('A%'))
|
638
|
+
# SELECT artist_id, COUNT(*) AS count FROM albums
|
639
|
+
# WHERE name LIKE 'A%' GROUP BY artist_id HAVING count >= 10
|
640
|
+
|
641
|
+
Both the WHERE clause and the HAVING clause are removed by +unfiltered+:
|
642
|
+
|
643
|
+
Album.group_and_count(:artist_id).having{count >= 10}.
|
644
|
+
where(:name.like('A%')).unfiltered
|
645
|
+
# SELECT artist_id, COUNT(*) AS count FROM albums GROUP BY artist_id
|
646
|
+
|
647
|
+
== Joins
|
648
|
+
|
649
|
+
Sequel makes it very easy to join a dataset to another table or dataset.
|
650
|
+
The underlying method used is +join_table+:
|
651
|
+
|
652
|
+
Album.join_table(:inner, :artists, :id=>:artist_id)
|
653
|
+
# SELECT * FROM albums
|
654
|
+
# INNER JOIN artists ON artists.id = albums.artist_id
|
655
|
+
|
656
|
+
In most cases, you won't call +join_table+ directly, as Sequel provides
|
657
|
+
shortcuts for all common (and most uncommon) join types. For example
|
658
|
+
+join+ does an inner join:
|
659
|
+
|
660
|
+
Album.join(:artists, :id=>:artist_id)
|
661
|
+
# SELECT * FROM albums
|
662
|
+
# INNER JOIN artists ON artists.id = albums.artist_id
|
663
|
+
|
664
|
+
And +left_join+ does a LEFT JOIN:
|
665
|
+
|
666
|
+
Album.left_join(:artists, :id=>:artist_id)
|
667
|
+
# SELECT * FROM albums
|
668
|
+
# LEFT JOIN artists ON artists.id = albums.artist_id
|
669
|
+
|
670
|
+
=== Table/Dataset to Join
|
671
|
+
|
672
|
+
For all of these specialized join methods, the first argument is
|
673
|
+
generally the name of the table to which you are joining. However, you
|
674
|
+
can also provide a model class:
|
675
|
+
|
676
|
+
Album.join(Artist, :id=>:artist_id)
|
677
|
+
|
678
|
+
Or a dataset, in which case a subselect is used:
|
679
|
+
|
680
|
+
Album.join(Artist.filter{name < 'A'}, :id=>:artist_id)
|
681
|
+
# SELECT * FROM albums
|
682
|
+
# INNER JOIN (SELECT * FROM artists WHERE (name < 'A')) AS t1
|
683
|
+
# ON (t1.id = albums.artist_id)
|
684
|
+
|
685
|
+
=== Join Conditions
|
686
|
+
|
687
|
+
The second argument to the specialized join methods is the conditions
|
688
|
+
to use when joining, which is similar to a filter expression, with
|
689
|
+
a few minor exceptions.
|
690
|
+
|
691
|
+
==== Implicit Qualification
|
692
|
+
|
693
|
+
A hash used as the join conditions operates similarly to a filter,
|
694
|
+
except that unqualified symbol keys are automatically qualified
|
695
|
+
with the table from the first argument, and unqualified symbol values
|
696
|
+
are automatically qualified with the first table or the last table
|
697
|
+
joined. This implicit qualification is one of the reasons that joins
|
698
|
+
in Sequel are easy to specify:
|
699
|
+
|
700
|
+
Album.join(:artists, :id=>:artist_id)
|
701
|
+
# SELECT * FROM albums
|
702
|
+
# INNER JOIN artists ON artists.id = albums.artist_id
|
703
|
+
|
704
|
+
Note how the <tt>:id</tt> symbol is automatically qualified with +artists+,
|
705
|
+
while the +artist_id+ symbol is automatically qualified with +albums+.
|
706
|
+
|
707
|
+
Because Sequel uses the last joined table for implicit qualifications
|
708
|
+
of values, you can do things like:
|
709
|
+
|
710
|
+
Album.join(:artists, :id=>:artist_id).
|
711
|
+
join(:members, :artist_id=>:id)
|
712
|
+
# SELECT * FROM albums
|
713
|
+
# INNER JOIN artists ON artists.id = albums.artist_id
|
714
|
+
# INNER JOIN members ON members.artist_id = artists.id
|
715
|
+
|
716
|
+
Note that when joining to the +members+ table, +artist_id+ is qualified
|
717
|
+
with +members+ and +id+ is qualified with +artists+.
|
718
|
+
|
719
|
+
While a good default, implicit qualification is not always correct:
|
720
|
+
|
721
|
+
Album.join(:artists, :id=>:artist_id).
|
722
|
+
join(:tracks, :album_id=>:id)
|
723
|
+
# SELECT * FROM albums
|
724
|
+
# INNER JOIN artists ON artists.id = albums.artist_id
|
725
|
+
# INNER JOIN tracks ON tracks.album_id = artists.id
|
726
|
+
|
727
|
+
Note here how +id+ is qualified with +artists+ instead of +albums+. This
|
728
|
+
is wrong as the foreign key <tt>tracks.album_id</tt> refers to <tt>albums.id</tt>, not
|
729
|
+
<tt>artists.id</tt>. To fix this, you need to explicitly qualify when joining:
|
730
|
+
|
731
|
+
Album.join(:artists, :id=>:artist_id).
|
732
|
+
join(:tracks, :album_id=>:albums__id)
|
733
|
+
# SELECT * FROM albums
|
734
|
+
# INNER JOIN artists ON artists.id = albums.artist_id
|
735
|
+
# INNER JOIN tracks ON tracks.album_id = albums.id
|
736
|
+
|
737
|
+
Just like in filters, an array of two element arrays is treated the same
|
738
|
+
as a hash, but allows for duplicate keys:
|
739
|
+
|
740
|
+
Album.join(:artists, [[:id, :artist_id], [:id, 1..5]])
|
741
|
+
# SELECT * FROM albums INNER JOIN artists
|
742
|
+
# ON artists.id = albums.artist_id
|
743
|
+
# AND artists.id >= 1 AND artists.id <= 5
|
744
|
+
|
745
|
+
And just like in the hash case, unqualified symbol elements in the
|
746
|
+
array are implicitly qualified.
|
747
|
+
|
748
|
+
==== USING Joins
|
749
|
+
|
750
|
+
The most common type of join conditions is a JOIN ON, as displayed
|
751
|
+
above. However, the SQL standard allows for join conditions to be
|
752
|
+
specified with JOIN USING, which Sequel makes easy to use.
|
753
|
+
|
754
|
+
JOIN USING is useful when the columns you are using have the same
|
755
|
+
names in both tables. For example, if instead of having a primary
|
756
|
+
column named +id+ in all of your tables, you use +artist_id+ in your
|
757
|
+
+artists+ table and +album_id+ in your +albums+ table, you could do:
|
758
|
+
|
759
|
+
Album.join(:artists, [:artist_id])
|
760
|
+
# SELECT * FROM albums INNER JOIN artists USING (artist_id)
|
761
|
+
|
762
|
+
See here how you specify the USING columns as an array of symbols.
|
763
|
+
|
764
|
+
==== NATURAL Joins
|
765
|
+
|
766
|
+
NATURAL Joins take it one step further than USING joins, by assuming
|
767
|
+
that all columns with the same names in both tables should be
|
768
|
+
used for joining:
|
769
|
+
|
770
|
+
Album.natural_join(:artists)
|
771
|
+
# SELECT * FROM albums NATURAL JOIN artists
|
772
|
+
|
773
|
+
In this case, you don't even need to specify any conditions.
|
774
|
+
|
775
|
+
==== Join Blocks
|
776
|
+
|
777
|
+
You can provide a block to any of the join methods that accept
|
778
|
+
conditions. This block should accept 3 arguments, the table alias
|
779
|
+
for the table currently being joined, the table alias for the last
|
780
|
+
table joined (or first table), and an array of previous
|
781
|
+
<tt>Sequel::SQL::JoinClause</tt>s.
|
782
|
+
|
783
|
+
This allows you to qualify columns similar to how the implicit
|
784
|
+
qualification works, without worrying about the specific aliases
|
785
|
+
being used. For example, lets say you wanted to join the albums
|
786
|
+
and artists tables, but only want albums where the artist's name
|
787
|
+
comes before the album's name.
|
788
|
+
|
789
|
+
Album.join(:artists, :id=>:artist_id) do |j, lj, js|
|
790
|
+
:name.qualify(j) < :name.qualify(lj)
|
791
|
+
end
|
792
|
+
# SELECT * FROM albums INNER JOIN artists
|
793
|
+
# ON artists.id = albums.artist_id
|
794
|
+
# AND artists.name < albums.name
|
795
|
+
|
796
|
+
Because greater than can't be expressed with a hash in Sequel, you
|
797
|
+
need to use a block and qualify the tables manually.
|
798
|
+
|
799
|
+
== From
|
800
|
+
|
801
|
+
In general, the FROM table is the first clause populated when creating
|
802
|
+
a dataset. For a standard Sequel::Model, the dataset already has the
|
803
|
+
FROM clause populated, and the most common way to create datasets is
|
804
|
+
with the <tt>Database#[]</tt> method, which populates the FROM clause.
|
805
|
+
|
806
|
+
However, you can modify the tables you are selecting FROM using +from+:
|
807
|
+
|
808
|
+
Album.from(:albums, :old_albums)
|
809
|
+
# SELECT * FROM albums, old_albums
|
810
|
+
|
811
|
+
Be careful with this, as multiple tables in the FROM clause use a cross
|
812
|
+
join by default, so the number of rows will be number of albums times the
|
813
|
+
number of old albums.
|
814
|
+
|
815
|
+
Using multiple FROM tables and setting conditions in the WHERE clause is
|
816
|
+
an old-school way of joining tables:
|
817
|
+
|
818
|
+
DB.from(:albums, :artists).where(:artists__id=>:albums__artist_id)
|
819
|
+
# SELECT * FROM albums, artists WHERE artists.id = albums.artist_id
|
820
|
+
|
821
|
+
=== Using the current dataset in a subselect
|
822
|
+
|
823
|
+
In some cases, you may want to wrap the current dataset in a subselect.
|
824
|
+
Here's an example using +from_self+:
|
825
|
+
|
826
|
+
Album.order(:artist_id).limit(100).from_self.group(:artist_id)
|
827
|
+
# SELECT * FROM (SELECT * FROM albums ORDER BY artist_id LIMIT 100)
|
828
|
+
# AS t1 GROUP BY artist_id
|
829
|
+
|
830
|
+
This is slightly different than without +from_self+:
|
831
|
+
|
832
|
+
Album.order(:artist_id).limit(100).group(:artist_id)
|
833
|
+
# SELECT * FROM albums GROUP BY artist_id ORDER BY name LIMIT 100
|
834
|
+
|
835
|
+
Without +from_self+, you are doing the grouping, and limiting the number
|
836
|
+
of grouped records returned to 100. So assuming you have albums by more
|
837
|
+
than 100 artists, you'll end up with 100 results.
|
838
|
+
|
839
|
+
With +from_self+, you are limiting the number of records before grouping.
|
840
|
+
So if the artist with the lowest id had 100 albums, you'd get 1 result,
|
841
|
+
not 100.
|
842
|
+
|
843
|
+
== Locking for Update
|
844
|
+
|
845
|
+
Sequel allows you to easily add a FOR UPDATE clause to your queries so
|
846
|
+
that the records returned can't be modified by another query until the
|
847
|
+
current transaction commits. You just use the +for_update+ dataset
|
848
|
+
method when returning the rows:
|
849
|
+
|
850
|
+
DB.transaction do
|
851
|
+
album = Album.for_update.first(:id=>1)
|
852
|
+
# SELECT * FROM albums WHERE id = 1 FOR UPDATE
|
853
|
+
album.num_tracks += 1
|
854
|
+
album.save
|
855
|
+
end
|
856
|
+
|
857
|
+
This will ensure that no other connection modifies the row between when you select
|
858
|
+
it and when the transaction ends.
|
859
|
+
|
860
|
+
=== Optimistic Locking
|
861
|
+
|
862
|
+
One of Sequel's built-in model plugins is an optimistic locking plugin, which provides
|
863
|
+
a database independent way to detect and raise an error if two different connections
|
864
|
+
modify the same row. It's useful for things like web forms where you cannot keep a
|
865
|
+
transaction open while the user is looking at the form, because of the web's
|
866
|
+
stateless nature.
|
867
|
+
|
868
|
+
== Custom SQL
|
869
|
+
|
870
|
+
Sequel makes it easy to use custom SQL by providing it to the <tt>Database#[]</tt>
|
871
|
+
method as a string:
|
872
|
+
|
873
|
+
DB["SELECT * FROM artists"]
|
874
|
+
# SELECT * FROM artists
|
875
|
+
|
876
|
+
You can also use the +with_sql+ dataset method to return a dataset that uses that
|
877
|
+
exact SQL:
|
878
|
+
|
879
|
+
DB[:albums].with_sql("SELECT * FROM artists")
|
880
|
+
# SELECT * FROM artists
|
881
|
+
|
882
|
+
With either of these methods, you can use placeholders:
|
883
|
+
|
884
|
+
DB["SELECT * FROM artists WHERE id = ?", 5]
|
885
|
+
# SELECT * FROM artists WHERE id = 5
|
886
|
+
|
887
|
+
DB[:albums].with_sql("SELECT * FROM artists WHERE id = :id", :id=>5)
|
888
|
+
# SELECT * FROM artists WHERE id = 5
|
889
|
+
|
890
|
+
== Checking for Records
|
891
|
+
|
892
|
+
If you just want to know whether the current dataset would return any rows, use <tt>empty?</tt>:
|
893
|
+
|
894
|
+
Album.empty?
|
895
|
+
# SELECT 1 FROM albums LIMIT 1
|
896
|
+
=> false
|
897
|
+
|
898
|
+
Album.filter(:id=>0).empty?
|
899
|
+
# SELECT 1 FROM albums WHERE id = 0 LIMIT 1
|
900
|
+
=> true
|
901
|
+
|
902
|
+
Album.filter(:name.like('R%')).empty?
|
903
|
+
# SELECT 1 FROM albums WHERE name LIKE 'R%' LIMIT 1
|
904
|
+
=> false
|
905
|
+
|
906
|
+
== Aggregate Calculations
|
907
|
+
|
908
|
+
The SQL standard defines a few helpful methods to get aggreate information about
|
909
|
+
datasets, such as +count+, +sum+, +avg+, +min+, and +max+. There are dataset methods
|
910
|
+
for each of these aggregate functions.
|
911
|
+
|
912
|
+
+count+ just returns the number of records in the dataset.
|
913
|
+
|
914
|
+
Album.count
|
915
|
+
# SELECT COUNT(*) AS count FROM albums LIMIT 1
|
916
|
+
=> 2
|
917
|
+
|
918
|
+
The other methods take a column argument and call the aggregate function with
|
919
|
+
the argument:
|
920
|
+
|
921
|
+
Album.sum(:id)
|
922
|
+
# SELECT sum(id) FROM albums LIMIT 1
|
923
|
+
=> 3
|
924
|
+
|
925
|
+
Album.avg(:id)
|
926
|
+
# SELECT avg(id) FROM albums LIMIT 1
|
927
|
+
=> 1.5
|
928
|
+
|
929
|
+
Album.min(:id)
|
930
|
+
# SELECT min(id) FROM albums LIMIT 1
|
931
|
+
=> 1
|
932
|
+
|
933
|
+
Album.max(:id)
|
934
|
+
# SELECT max(id) FROM albums LIMIT 1
|
935
|
+
=> 2
|