sequel 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,59 @@
1
+ === HEAD
2
+
3
+ * Make the validation errors API compatible with Merb (Inviz)
4
+
5
+ * Add validates_uniqueness_of, for protecting against duplicate entries in the database (neaf, jeremyevans)
6
+
7
+ * Alias Model#dataset= to Model#set_dataset (tmm1)
8
+
9
+ * Make some Model class methods private: def_hook_method, hooks, add_hook, plugin_module, plugin_gem (jeremyevans)
10
+
11
+ * Add the eager! and eager_graph! mutation methods to model datasets (jeremyevans)
12
+
13
+ * Remove Model.database_opened (jeremyevans)
14
+
15
+ * Remove Model.super_dataset (jeremyevans)
16
+
17
+ * Deprecate .create_with_params, .create_with, #set, #update, #update_with, and #new_record from Sequel::Model (jeremyevans)
18
+
19
+ * Add Model.def_dataset_method, for defining methods on the model that reference methods on the dataset (jeremyevans)
20
+
21
+ * Deprecate Model.method_missing, add dataset methods to Model via metaprogramming (jeremyevans)
22
+
23
+ * Remove Model.join, so it is the same as Dataset#join (jeremyevans)
24
+
25
+ * Use reciprocal associations for all types of associations in the getter/setter/add_/remove_ methods (jeremyevans)
26
+
27
+ * Fix many_to_one associations to cache negative lookups (jeremyevans)
28
+
29
+ * Change Model#=== to always be false if the primary key is nil (jeremyevans)
30
+
31
+ * Add Model#hash, which should be unique for a given class and primary key (or values if primary key is nil) (jeremyevans)
32
+
33
+ * Add Model#eql? as a alias to Model#== (jeremyevans)
34
+
35
+ * Make Model#reload clear any cached associations (jeremyevans)
36
+
37
+ * No longer depend on the assistance gem, merge the Inflector and Validations code (jeremyevans)
38
+
39
+ * Add Model#set_with_params, which is Model#update_with_params without the save (jeremyevans)
40
+
41
+ * Fix Model#destroy so that it returns self, not the result of after_destroy (jeremyevans)
42
+
43
+ * Define Model column accessors in set_dataset, so they should always be avaiable, deprecate Model#method_missing (jeremyevans)
44
+
45
+ * Add eager loading of associations via new sequel_core object graphing feature (jeremyevans)
46
+
47
+ * Fix many_to_many associations with classes inside modules without an explicit join table (jeremyevans)
48
+
49
+ * Allow creation of new records that don't have primary keys when the cache is on (jeremyevans) (#213)
50
+
51
+ * Make Model#initialize, Model#set, and Model#update_with_params invulnerable to memory exhaustion (jeremyevans) (#210)
52
+
53
+ * Add Model.str_columns, which gives a list of columns as frozen strings (jeremyevans)
54
+
55
+ * Remove pretty_table.rb from sequel, since it is in sequel_core (jeremyevans)
56
+
1
57
  === 1.4.0 (2008-04-08)
2
58
 
3
59
  * Don't mark a column as changed unless the new value is different from the current value (tamas.denes, jeremyevans) (#203).
data/README CHANGED
@@ -1,10 +1,31 @@
1
- == Sequel: The Database Toolkit for Ruby
1
+ == Sequel Models
2
2
 
3
- Sequel is a database access toolkit for Ruby. Sequel provides thread safety, connection pooling, and a concise DSL for constructing queries and table schemas.
3
+ Models in Sequel are based on the Active Record pattern described by Martin Fowler (http://www.martinfowler.com/eaaCatalog/activeRecord.html). A model class corresponds to a table or a dataset, and an instance of that class wraps a single record in the model's underlying dataset.
4
4
 
5
- Sequel makes it easy to deal with multiple records without having to break your teeth on SQL.
5
+ Model classes are defined as regular Ruby classes:
6
6
 
7
- == Resources
7
+ DB = Sequel.connect('sqlite:/blog.db')
8
+ class Post < Sequel::Model
9
+ end
10
+
11
+ Just like in DataMapper or ActiveRecord, Sequel model classes assume that the table name is a plural of the class name:
12
+
13
+ Post.table_name #=> :posts
14
+
15
+ You can, however, explicitly set the table name or even the dataset used:
16
+
17
+ class Post < Sequel::Model(:my_posts)
18
+ end
19
+
20
+ # or:
21
+
22
+ Post.set_dataset :my_posts
23
+
24
+ # or:
25
+
26
+ Post.set_dataset DB[:my_posts].where(:category => 'ruby')
27
+
28
+ === Resources
8
29
 
9
30
  * {Project page}[http://code.google.com/p/ruby-sequel/]
10
31
  * {Source code}[http://github.com/jeremyevans/sequel]
@@ -14,239 +35,321 @@ Sequel makes it easy to deal with multiple records without having to break your
14
35
  * {API RDoc}[http://sequel.rubyforge.org]
15
36
 
16
37
  To check out the source code:
17
-
38
+
18
39
  git clone git://github.com/jeremyevans/sequel.git
19
-
40
+
20
41
  === Contact
21
42
 
22
43
  If you have any comments or suggestions please post to the Google group.
23
44
 
24
- == Installation
45
+ === Installation
25
46
 
26
47
  sudo gem install sequel
27
-
28
- == Supported Databases
29
48
 
30
- Sequel currently supports:
49
+ === Model instances
31
50
 
32
- * ADO (on Windows)
33
- * DBI
34
- * Informix
35
- * MySQL
36
- * ODBC
37
- * Oracle
38
- * PostgreSQL
39
- * SQLite 3
51
+ Model instance are identified by a primary key. By default, Sequel assumes the primary key column to be :id. The Model#[] method can be used to fetch records by their primary key:
40
52
 
41
- There are also experimental adapters for DB2, OpenBase and JDBC (on JRuby).
53
+ post = Post[123]
42
54
 
43
- == The Sequel Console
55
+ The Model#pk method is used to retrieve the record's primary key value:
44
56
 
45
- Sequel includes an IRB console for quick'n'dirty access to databases. You can use it like this:
57
+ post.pk #=> 123
46
58
 
47
- sequel sqlite:///test.db
59
+ Sequel models allow you to use any column as a primary key, and even composite keys made from multiple columns:
48
60
 
49
- You get an IRB session with the database object stored in DB.
61
+ class Post < Sequel::Model
62
+ set_primary_key [:category, :title]
63
+ end
50
64
 
51
- == An Introduction
65
+ post = Post['ruby', 'hello world']
66
+ post.pk #=> ['ruby', 'hello world']
52
67
 
53
- Sequel is designed to take the hassle away from connecting to databases and manipulating them. Sequel deals with all the boring stuff like maintaining connections, formatting SQL correctly and fetching records so you can concentrate on your application.
68
+ You can also define a model class that does not have a primary key, but then you lose the ability to update records.
54
69
 
55
- Sequel uses the concept of datasets to retrieve data. A Dataset object encapsulates an SQL query and supports chainability, letting you fetch data using a convenient Ruby DSL that is both concise and infinitely flexible.
70
+ A model instance can also be fetched by specifying a condition:
56
71
 
57
- For example, the following one-liner returns the average GDP for the five biggest countries in the middle east region:
72
+ post = Post[:title => 'hello world']
73
+ post = Post.find{:num_comments < 10}
58
74
 
59
- DB[:countries].filter(:region => 'Middle East').reverse_order(:area).limit(5).avg(:GDP)
60
-
61
- Which is equivalent to:
75
+ === Iterating over records
62
76
 
63
- SELECT avg(GDP) FROM countries WHERE region = 'Middle East' ORDER BY area DESC LIMIT 5
77
+ A model class lets you iterate over specific records by acting as a proxy to the underlying dataset. This means that you can use the entire Dataset API to create customized queries that return model instances, e.g.:
64
78
 
65
- Since datasets retrieve records only when needed, they can be stored and later reused. Records are fetched as hashes (they can also be fetched as custom model objects), and are accessed using an Enumerable interface:
79
+ Post.filter(:category => 'ruby').each{|post| p post}
66
80
 
67
- middle_east = DB[:countries].filter(:region => 'Middle East')
68
- middle_east.order(:name).each {|r| puts r[:name]}
69
-
70
- Sequel also offers convenience methods for extracting data from Datasets, such as an extended map method:
81
+ You can also manipulate the records in the dataset:
71
82
 
72
- middle_east.map(:name) #=> ['Egypt', 'Greece', 'Israel', ...]
73
-
74
- Or getting results as a transposed hash, with one column as key and another as value:
83
+ Post.filter{:num_comments < 7}.delete
84
+ Post.filter{:title =~ /ruby/}.update(:category => 'ruby')
75
85
 
76
- middle_east.to_hash(:name, :area) #=> {'Israel' => 20000, 'Greece' => 120000, ...}
86
+ === Accessing record values
77
87
 
78
- Much of Sequel is still undocumented (especially the part relating to model classes). The following section provides examples of common usage. Feel free to explore...
88
+ A model instances stores its values as a hash:
79
89
 
80
- == Getting Started
90
+ post.values #=> {:id => 123, :category => 'ruby', :title => 'hello world'}
81
91
 
82
- === Connecting to a database
92
+ You can read the record values as object attributes (assuming the attribute names are valid columns in the model's dataset):
83
93
 
84
- To connect to a database you simply provide Sequel with a URL:
94
+ post.id #=> 123
95
+ post.title #=> 'hello world'
85
96
 
86
- require 'sequel'
87
- DB = Sequel.open 'sqlite:///blog.db'
88
-
89
- The connection URL can also include such stuff as the user name and password:
97
+ You can also change record values:
90
98
 
91
- DB = Sequel.open 'postgres://cico:12345@localhost:5432/mydb'
99
+ post.title = 'hey there'
100
+ post.save
92
101
 
93
- You can also specify optional parameters, such as the connection pool size, or a logger for logging SQL queries:
102
+ Another way to change values by using the #update_with_params method:
94
103
 
95
- DB = Sequel.open("postgres://postgres:postgres@localhost/my_db",
96
- :max_connections => 10, :logger => Logger.new('log/db.log'))
104
+ post.update_with_params(:title => 'hey there')
97
105
 
98
- === Arbitrary SQL queries
106
+ === Creating new records
99
107
 
100
- DB.execute("create table t (a text, b text)")
101
- DB.execute("insert into t values ('a', 'b')")
108
+ New records can be created by calling Model.create:
102
109
 
103
- Or more succinctly:
110
+ post = Post.create(:title => 'hello world')
104
111
 
105
- DB << "create table t (a text, b text)"
106
- DB << "insert into t values ('a', 'b')"
112
+ Another way is to construct a new instance and save it:
107
113
 
108
- === Getting Dataset Instances
114
+ post = Post.new
115
+ post.title = 'hello world'
116
+ post.save
109
117
 
110
- Dataset is the primary means through which records are retrieved and manipulated. You can create an blank dataset by using the dataset method:
118
+ You can also supply a block to Model.new and Model.create:
111
119
 
112
- dataset = DB.dataset
120
+ post = Post.create {|p| p.title = 'hello world'}
113
121
 
114
- Or by using the from methods:
122
+ post = Post.new do |p|
123
+ p.title = 'hello world'
124
+ p.save
125
+ end
115
126
 
116
- posts = DB.from(:posts)
127
+ === Hooks
117
128
 
118
- You can also use the equivalent shorthand:
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.
119
130
 
120
- posts = DB[:posts]
131
+ Hooks are defined by supplying a block:
121
132
 
122
- Note: the dataset will only fetch records when you explicitly ask for them, as will be shown below. Datasets can be manipulated to filter through records, change record order and even join tables, as will also be shown below.
133
+ class Post < Sequel::Model
134
+ after_create do
135
+ self.created_at = Time.now
136
+ end
123
137
 
124
- === Retrieving Records
138
+ after_destroy do
139
+ author.update_post_count
140
+ end
141
+ end
125
142
 
126
- You can retrieve records by using the all method:
143
+ === Deleting records
127
144
 
128
- posts.all
145
+ You can delete individual records by calling #delete or #destroy. The only difference between the two methods is that #destroy invokes before_destroy and after_destroy hooks, while #delete does not:
129
146
 
130
- The all method returns an array of hashes, where each hash corresponds to a record.
147
+ post.delete #=> bypasses hooks
148
+ post.destroy #=> runs hooks
131
149
 
132
- You can also iterate through records one at a time:
150
+ Records can also be deleted en-masse by invoking Model.delete and Model.destroy. As stated above, you can specify filters for the deleted records:
133
151
 
134
- posts.each {|row| p row}
152
+ Post.filter(:category => 32).delete #=> bypasses hooks
153
+ Post.filter(:category => 32).destroy #=> runs hooks
135
154
 
136
- Or perform more advanced stuff:
155
+ Please note that if Model.destroy is called, each record is deleted
156
+ separately, but Model.delete deletes all relevant records with a single
157
+ SQL statement.
137
158
 
138
- posts.map(:id)
139
- posts.inject({}) {|h, r| h[r[:id]] = r[:name]}
140
-
141
- You can also retrieve the first record in a dataset:
159
+ === Associations
142
160
 
143
- posts.first
144
-
145
- Or retrieve a single record with a specific value:
161
+ Associations are used in order to specify relationships between model classes that reflect relations between tables in the database using foreign keys.
146
162
 
147
- posts[:id => 1]
148
-
149
- If the dataset is ordered, you can also ask for the last record:
163
+ class Post < Sequel::Model
164
+ many_to_one :author
165
+ one_to_many :comments
166
+ many_to_many :tags
167
+ end
150
168
 
151
- posts.order(:stamp).last
152
-
153
- === Filtering Records
169
+ You can also use the ActiveRecord names for these associations:
154
170
 
155
- The simplest way to filter records is to provide a hash of values to match:
171
+ class Post < Sequel::Model
172
+ belongs_to :author
173
+ has_many :comments
174
+ has_and_belongs_to_many :tags
175
+ end
156
176
 
157
- my_posts = posts.filter(:category => 'ruby', :author => 'david')
158
-
159
- You can also specify ranges:
177
+ many_to_one/belongs_to creates a getter and setter for each model object:
160
178
 
161
- my_posts = posts.filter(:stamp => (2.weeks.ago)..(1.week.ago))
162
-
163
- Or lists of values:
179
+ class Post < Sequel::Model
180
+ many_to_one :author
181
+ end
164
182
 
165
- my_posts = posts.filter(:category => ['ruby', 'postgres', 'linux'])
166
-
167
- Sequel now also accepts expressions as closures, AKA block filters:
183
+ post = Post.create(:name => 'hi!')
184
+ post.author = Author[:name => 'Sharon']
185
+ post.author
168
186
 
169
- my_posts = posts.filter {:category == ['ruby', 'postgres', 'linux']}
170
-
171
- Which also lets you do stuff like:
187
+ one_to_many/has_many and many_to_many/has_and_belongs_to_many create a getter method, a method for adding an object to the association, and a method for removing an object from the association:
172
188
 
173
- my_posts = posts.filter {:stamp > 1.month.ago}
174
-
175
- Some adapters (like postgresql) will also let you specify Regexps:
189
+ class Post < Sequel::Model
190
+ one_to_many :comments
191
+ many_to_many :tags
192
+ end
176
193
 
177
- my_posts = posts.filter(:category => /ruby/i)
178
-
179
- You can also use an inverse filter:
180
-
181
- my_posts = posts.exclude(:category => /ruby/i)
194
+ post = Post.create(:name => 'hi!')
195
+ post.comments
196
+ comment = Comment.create(:text=>'hi')
197
+ post.add_comment(comment)
198
+ post.remove_comment(comment)
199
+ tag = Tag.create(:tag=>'interesting')
200
+ post.add_tag(tag)
201
+ post.remove_tag(tag)
182
202
 
183
- You can then retrieve the records by using any of the retrieval methods:
203
+ === Eager Loading
184
204
 
185
- my_posts.each {|row| p row}
186
-
187
- You can also specify a custom WHERE clause:
205
+ Associations can be eagerly loaded via .eager and the :eager association option. Eager loading is used when loading a group of objects. It loads all associated objects for all of the current objects in one query, instead of using a separate query to get the associated objects for each current object. Eager loading requires that you retrieve all model objects at once via .all (instead of individually by .each). Eager loading can be cascaded, loading association's associated objects.
188
206
 
189
- posts.filter('(stamp < ?) AND (author <> ?)', 3.days.ago, author_name)
207
+ class Person < Sequel::Model
208
+ one_to_many :posts, :eager=>[:tags]
209
+ end
190
210
 
191
- Datasets can also be used as subqueries:
211
+ class Post < Sequel::Model
212
+ many_to_one :person
213
+ one_to_many :replies
214
+ many_to_many :tags
215
+ end
192
216
 
193
- DB[:items].filter('price > ?', DB[:items].select('AVG(price) + 100'))
217
+ class Tag < Sequel::Model
218
+ many_to_many :posts
219
+ many_to_many :replies
220
+ end
194
221
 
195
- === Summarizing Records
222
+ class Reply < Sequel::Model
223
+ many_to_one :person
224
+ many_to_one :post
225
+ many_to_many :tags
226
+ end
196
227
 
197
- Counting records is easy:
198
- posts.filter(:category => /ruby/i).count
228
+ # Eager loading via .eager
229
+ Post.eager(:person).all
199
230
 
200
- And you can also query maximum/minimum values:
201
- max_value = DB[:history].max(:value)
231
+ # eager is a dataset method, so it works with filters/orders/limits/etc.
232
+ Post.filter("topic > 'M'").order(:date).limit(5).eager(:person).all
202
233
 
203
- Or calculate a sum:
204
- total = DB[:items].sum(:price)
234
+ person = Person.first
235
+ # Eager loading via :eager (will eagerly load the tags for this person's posts)
236
+ person.posts
205
237
 
206
- === Ordering Records
207
-
208
- posts.order(:stamp)
238
+ # These are equivalent
239
+ Post.eager(:person, :tags).all
240
+ Post.eager(:person).eager(:tags).all
209
241
 
210
- You can also specify descending order
242
+ # Cascading via .eager
243
+ Tag.eager(:posts=>:replies).all
244
+
245
+ # Will also grab all associated posts' tags (because of :eager)
246
+ Reply.eager(:person=>:posts).all
247
+
248
+ # No depth limit (other than memory/stack), and will also grab posts' tags
249
+ # Loads all people, their posts, their posts' tags, replies to those posts,
250
+ # the person for each reply, the tag for each reply, and all posts and
251
+ # replies that have that tag. Uses a total of 8 queries.
252
+ Person.eager(:posts=>{:replies=>[:person, {:tags=>{:posts, :replies}}]}).all
211
253
 
212
- posts.order(:stamp.desc)
254
+ In addition to using eager, you can also use eager_graph, which will use a single query to get the object and all associated objects. This may be necessary if you want to filter the result set based on columns in associated tables. It works with cascading as well, the syntax is exactly the same. Note that using eager_graph to eagerly load multiple *_to_many associations will cause the result set to be a cartesian product, so you should be very careful with your filters when using it in that case.
213
255
 
214
- === Deleting Records
256
+ === Caching model instances with memcached
215
257
 
216
- posts.filter('stamp < ?', 3.days.ago).delete
217
-
218
- === Inserting Records
258
+ Sequel models can be cached using memcached based on their primary keys. The use of memcached can significantly reduce database load by keeping model instances in memory. The set_cache method is used to specify caching:
219
259
 
220
- posts.insert(:category => 'ruby', :author => 'david')
221
-
222
- Or alternatively:
260
+ require 'memcache'
261
+ CACHE = MemCache.new 'localhost:11211', :namespace => 'blog'
223
262
 
224
- posts << {:category => 'ruby', :author => 'david'}
225
-
226
- === Updating Records
263
+ class Author < Sequel::Model
264
+ set_cache CACHE, :ttl => 3600
265
+ end
227
266
 
228
- posts.filter('stamp < ?', 3.days.ago).update(:state => 'archived')
267
+ Author[333] # database hit
268
+ Author[333] # cache hit
229
269
 
230
- === Joining Tables
270
+ === Extending the underlying dataset
231
271
 
232
- Joining is very useful in a variety of scenarios, for example many-to-many relationships. With Sequel it's really easy:
272
+ The obvious way to add table-wide logic is to define class methods to the model class definition. That way you can define subsets of the underlying dataset, change the ordering, or perform actions on multiple records:
233
273
 
234
- order_items = DB[:items].join(:order_items, :item_id => :id).
235
- filter(:order_items__order_id => 1234)
236
-
237
- This is equivalent to the SQL:
274
+ class Post < Sequel::Model
275
+ def self.posts_with_few_comments
276
+ filter{:num_comments < 30}
277
+ end
238
278
 
239
- SELECT * FROM items LEFT OUTER JOIN order_items
240
- ON order_items.item_id = items.id
241
- WHERE order_items.order_id = 1234
279
+ def self.clean_posts_with_few_comments
280
+ posts_with_few_comments.delete
281
+ end
282
+ end
242
283
 
243
- You can then do anything you like with the dataset:
284
+ You can also implement table-wide logic by defining methods on the dataset:
244
285
 
245
- order_total = order_items.sum(:price)
246
-
247
- Which is equivalent to the SQL:
286
+ class Post < Sequel::Model
287
+ def_dataset_method(:posts_with_few_comments) do
288
+ filter{:num_comments < 30}
289
+ end
248
290
 
249
- SELECT sum(price) FROM items LEFT OUTER JOIN order_items
250
- ON order_items.item_id = items.id
251
- WHERE order_items.order_id = 1234
252
-
291
+ def_dataset_method(:clean_posts_with_few_comments) do
292
+ posts_with_few_comments.delete
293
+ end
294
+ end
295
+
296
+ This is the recommended way of implementing table-wide operations, and allows you to have access to your model API from filtered datasets as well:
297
+
298
+ Post.filter(:category => 'ruby').clean_old_posts
299
+
300
+ Sequel models also provide a short hand notation for filters:
301
+
302
+ class Post < Sequel::Model
303
+ subset(:posts_with_few_comments){:num_comments < 30}
304
+ subset :invisible, :visible => false
305
+ end
306
+
307
+ === Defining the underlying schema
308
+
309
+ Model classes can also be used as a place to define your table schema and control it. The schema DSL is exactly the same provided by Sequel::Schema::Generator:
310
+
311
+ class Post < Sequel::Model
312
+ set_schema do
313
+ primary_key :id
314
+ text :title
315
+ text :category
316
+ foreign_key :author_id, :table => :authors
317
+ end
318
+ end
319
+
320
+ You can then create the underlying table, drop it, or recreate it:
321
+
322
+ Post.table_exists?
323
+ Post.create_table
324
+ Post.drop_table
325
+ Post.create_table! # drops the table if it exists and then recreates it
326
+
327
+ === Basic Model Validations
328
+
329
+ To assign default validations to a sequel model:
330
+
331
+ class MyModel < Sequel::Model
332
+ validates do
333
+ format_of...
334
+ presence_of...
335
+ acceptance_of...
336
+ confirmation_of...
337
+ length_of...
338
+ numericality_of...
339
+ format_of...
340
+ each...
341
+ end
342
+ end
343
+
344
+ You may also perform the usual 'longhand' way to assign default model validates directly within the model class itself:
345
+
346
+ class MyModel < Sequel::Model
347
+ validates_format_of...
348
+ validates_presence_of...
349
+ validates_acceptance_of...
350
+ validates_confirmation_of...
351
+ validates_length_of...
352
+ validates_numericality_of...
353
+ validates_format_of...
354
+ validates_each...
355
+ end