sequel 5.83.0 → 5.84.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/sequel/adapters/shared/sqlite.rb +3 -1
- data/lib/sequel/database/connecting.rb +1 -1
- data/lib/sequel/database/misc.rb +8 -3
- data/lib/sequel/database/schema_methods.rb +2 -0
- data/lib/sequel/extensions/pg_json_ops.rb +328 -1
- data/lib/sequel/sql.rb +8 -5
- data/lib/sequel/version.rb +1 -1
- metadata +2 -236
- data/CHANGELOG +0 -1393
- data/README.rdoc +0 -936
- data/doc/advanced_associations.rdoc +0 -884
- data/doc/association_basics.rdoc +0 -1859
- data/doc/bin_sequel.rdoc +0 -146
- data/doc/cheat_sheet.rdoc +0 -255
- data/doc/code_order.rdoc +0 -104
- data/doc/core_extensions.rdoc +0 -405
- data/doc/dataset_basics.rdoc +0 -96
- data/doc/dataset_filtering.rdoc +0 -222
- data/doc/extensions.rdoc +0 -77
- data/doc/fork_safety.rdoc +0 -84
- data/doc/mass_assignment.rdoc +0 -98
- data/doc/migration.rdoc +0 -660
- data/doc/model_dataset_method_design.rdoc +0 -129
- data/doc/model_hooks.rdoc +0 -254
- data/doc/model_plugins.rdoc +0 -270
- data/doc/mssql_stored_procedures.rdoc +0 -43
- data/doc/object_model.rdoc +0 -563
- data/doc/opening_databases.rdoc +0 -439
- data/doc/postgresql.rdoc +0 -611
- data/doc/prepared_statements.rdoc +0 -144
- data/doc/querying.rdoc +0 -1070
- data/doc/reflection.rdoc +0 -120
- data/doc/release_notes/5.0.0.txt +0 -159
- data/doc/release_notes/5.1.0.txt +0 -31
- data/doc/release_notes/5.10.0.txt +0 -84
- data/doc/release_notes/5.11.0.txt +0 -83
- data/doc/release_notes/5.12.0.txt +0 -141
- data/doc/release_notes/5.13.0.txt +0 -27
- data/doc/release_notes/5.14.0.txt +0 -63
- data/doc/release_notes/5.15.0.txt +0 -39
- data/doc/release_notes/5.16.0.txt +0 -110
- data/doc/release_notes/5.17.0.txt +0 -31
- data/doc/release_notes/5.18.0.txt +0 -69
- data/doc/release_notes/5.19.0.txt +0 -28
- data/doc/release_notes/5.2.0.txt +0 -33
- data/doc/release_notes/5.20.0.txt +0 -89
- data/doc/release_notes/5.21.0.txt +0 -87
- data/doc/release_notes/5.22.0.txt +0 -48
- data/doc/release_notes/5.23.0.txt +0 -56
- data/doc/release_notes/5.24.0.txt +0 -56
- data/doc/release_notes/5.25.0.txt +0 -32
- data/doc/release_notes/5.26.0.txt +0 -35
- data/doc/release_notes/5.27.0.txt +0 -21
- data/doc/release_notes/5.28.0.txt +0 -16
- data/doc/release_notes/5.29.0.txt +0 -22
- data/doc/release_notes/5.3.0.txt +0 -121
- data/doc/release_notes/5.30.0.txt +0 -20
- data/doc/release_notes/5.31.0.txt +0 -148
- data/doc/release_notes/5.32.0.txt +0 -46
- data/doc/release_notes/5.33.0.txt +0 -24
- data/doc/release_notes/5.34.0.txt +0 -40
- data/doc/release_notes/5.35.0.txt +0 -56
- data/doc/release_notes/5.36.0.txt +0 -60
- data/doc/release_notes/5.37.0.txt +0 -30
- data/doc/release_notes/5.38.0.txt +0 -28
- data/doc/release_notes/5.39.0.txt +0 -19
- data/doc/release_notes/5.4.0.txt +0 -80
- data/doc/release_notes/5.40.0.txt +0 -40
- data/doc/release_notes/5.41.0.txt +0 -25
- data/doc/release_notes/5.42.0.txt +0 -136
- data/doc/release_notes/5.43.0.txt +0 -98
- data/doc/release_notes/5.44.0.txt +0 -32
- data/doc/release_notes/5.45.0.txt +0 -34
- data/doc/release_notes/5.46.0.txt +0 -87
- data/doc/release_notes/5.47.0.txt +0 -59
- data/doc/release_notes/5.48.0.txt +0 -14
- data/doc/release_notes/5.49.0.txt +0 -59
- data/doc/release_notes/5.5.0.txt +0 -61
- data/doc/release_notes/5.50.0.txt +0 -78
- data/doc/release_notes/5.51.0.txt +0 -47
- data/doc/release_notes/5.52.0.txt +0 -87
- data/doc/release_notes/5.53.0.txt +0 -23
- data/doc/release_notes/5.54.0.txt +0 -27
- data/doc/release_notes/5.55.0.txt +0 -21
- data/doc/release_notes/5.56.0.txt +0 -51
- data/doc/release_notes/5.57.0.txt +0 -23
- data/doc/release_notes/5.58.0.txt +0 -31
- data/doc/release_notes/5.59.0.txt +0 -73
- data/doc/release_notes/5.6.0.txt +0 -31
- data/doc/release_notes/5.60.0.txt +0 -22
- data/doc/release_notes/5.61.0.txt +0 -43
- data/doc/release_notes/5.62.0.txt +0 -132
- data/doc/release_notes/5.63.0.txt +0 -33
- data/doc/release_notes/5.64.0.txt +0 -50
- data/doc/release_notes/5.65.0.txt +0 -21
- data/doc/release_notes/5.66.0.txt +0 -24
- data/doc/release_notes/5.67.0.txt +0 -32
- data/doc/release_notes/5.68.0.txt +0 -61
- data/doc/release_notes/5.69.0.txt +0 -26
- data/doc/release_notes/5.7.0.txt +0 -108
- data/doc/release_notes/5.70.0.txt +0 -35
- data/doc/release_notes/5.71.0.txt +0 -21
- data/doc/release_notes/5.72.0.txt +0 -33
- data/doc/release_notes/5.73.0.txt +0 -66
- data/doc/release_notes/5.74.0.txt +0 -45
- data/doc/release_notes/5.75.0.txt +0 -35
- data/doc/release_notes/5.76.0.txt +0 -86
- data/doc/release_notes/5.77.0.txt +0 -63
- data/doc/release_notes/5.78.0.txt +0 -67
- data/doc/release_notes/5.79.0.txt +0 -28
- data/doc/release_notes/5.8.0.txt +0 -170
- data/doc/release_notes/5.80.0.txt +0 -40
- data/doc/release_notes/5.81.0.txt +0 -31
- data/doc/release_notes/5.82.0.txt +0 -61
- data/doc/release_notes/5.83.0.txt +0 -56
- data/doc/release_notes/5.9.0.txt +0 -99
- data/doc/schema_modification.rdoc +0 -679
- data/doc/security.rdoc +0 -443
- data/doc/sharding.rdoc +0 -286
- data/doc/sql.rdoc +0 -648
- data/doc/testing.rdoc +0 -204
- data/doc/thread_safety.rdoc +0 -15
- data/doc/transactions.rdoc +0 -250
- data/doc/validations.rdoc +0 -558
- data/doc/virtual_rows.rdoc +0 -265
@@ -1,129 +0,0 @@
|
|
1
|
-
= Model Dataset Method Design Guide
|
2
|
-
|
3
|
-
How you design your model dataset methods can significantly affect the flexibility of your API for your model classes, as well as the performance. The goal of this guide is to provide an example of how to design your model dataset methods for maximum flexibility and performance.
|
4
|
-
|
5
|
-
== Flexibility: Use Single Method Per Task
|
6
|
-
|
7
|
-
In general, it is recommended that you have a single method per task for maximum flexibility. For example, let's say you need to retrieve all albums released in a given year, ordered by number of units sold descending, and only care about the id, name and number of units sold. One way to do this is in your application code (outside the model), you can
|
8
|
-
call the dataset methods directly:
|
9
|
-
|
10
|
-
Album.
|
11
|
-
select(:id, :name, :copies_sold).
|
12
|
-
where(release_year: params[:year].to_i).
|
13
|
-
order(Sequel.desc(:copies_sold)).
|
14
|
-
all
|
15
|
-
|
16
|
-
One issue with this design is that it ties you to your current database schema, and will make it necessary to change your application code if your schema changes. In general, it is better to encapsulate your code into a dataset method (or a class method, but a dataset method is more flexible):
|
17
|
-
|
18
|
-
class Album < Sequel::Model
|
19
|
-
dataset_module do
|
20
|
-
def all_albums_released_in_year(year)
|
21
|
-
select(:id, :name, :copies_sold).
|
22
|
-
where(release_year: year).
|
23
|
-
order(Sequel.desc(:copies_sold)).
|
24
|
-
all
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
Then your application code just needs to call your dataset method:
|
30
|
-
|
31
|
-
Album.all_albums_released_in_year(params[:year].to_i)
|
32
|
-
|
33
|
-
The advantage of this approach is that you can change your schema at any point in the future, and you should only need to change your model code, you should never need to change other application code.
|
34
|
-
|
35
|
-
== Performance
|
36
|
-
|
37
|
-
After designing your dataset methods for flexibility, stop. Don't worry about performance until you need to worry about performance. However, assuming you have profiled your application and profiling shows you can benefit from optimizing the above method, you can then consider the performance impact of future design choices.
|
38
|
-
|
39
|
-
First, considering that the root cause of the performance issue may not be at the Sequel level, it may be at the database itself. Use +EXPLAIN+ or the equivalent to analyze the query plan for the query in use, and see if there is something you can do to optimize it, such as adding an appropriate index.
|
40
|
-
|
41
|
-
Second, assuming the performance issue is at the Sequel level, you need to understand that one of the best ways to improve performance in most ruby code is to reduce the number of objects allocated. Here is the above code with comments showing datasets allocated:
|
42
|
-
|
43
|
-
def all_albums_released_in_year(year)
|
44
|
-
select(:id, :name, :copies_sold). # new dataset allocated
|
45
|
-
where(release_year: year). # new dataset allocated
|
46
|
-
order(Sequel.desc(:copies_sold)). # new dataset allocated
|
47
|
-
all
|
48
|
-
end
|
49
|
-
|
50
|
-
Third, you need to understand that Sequel has optimizations specifically designed to reduce the number of objects allocated, by caching intermediate datasets. Unfortunately, those optimizations do not apply in this case. The reason for this is that +select+, +where+, and +order+ can potentially receive arbitrary arguments, and enabling caching for them could easily lead to unbounded cache size (denial of service due to memory exhaustion).
|
51
|
-
|
52
|
-
To allow intermediate dataset caching to work, you need to signal to Sequel that particular arguments to these methods should be cached, and you can do that by calling methods inside +dataset_module+ blocks such as +select+ and +order+. These methods will add dataset methods to the model that can cache the returned dataset to optimize performance. Here is an example using these methods:
|
53
|
-
|
54
|
-
class Album < Sequel::Model
|
55
|
-
dataset_module do
|
56
|
-
select :with_name_and_units, :id, :name, :copies_sold
|
57
|
-
order :by_units_sold, Sequel.desc(:copies_sold)
|
58
|
-
|
59
|
-
def all_albums_released_in_year(year)
|
60
|
-
with_name_and_units.
|
61
|
-
by_units_sold.
|
62
|
-
where(release_year: year).
|
63
|
-
all
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
Performance aside, this does provide a slightly nicer and more readable internal API, though naming such methods can be problematic.
|
69
|
-
|
70
|
-
By calling +select+ and +order+ here, Sequel expects that the created dataset methods may be called more than once on the same dataset, and it knows that the arguments to the underlying +select+ and +order+ methods are fixed, so it can cache the resulting datasets. Let's comment the above example with dataset allocations:
|
71
|
-
|
72
|
-
def all_albums_released_in_year(year)
|
73
|
-
with_name_and_units. # cached dataset returned
|
74
|
-
by_units_sold. # cached dataset returned
|
75
|
-
where(release_year: year). # new dataset allocated
|
76
|
-
all
|
77
|
-
end
|
78
|
-
|
79
|
-
Note that the order of methods here is important. If you instead change the method chain to filter the dataset first, then no caching happens:
|
80
|
-
|
81
|
-
def all_albums_released_in_year(year)
|
82
|
-
where(release_year: year). # new dataset allocated
|
83
|
-
with_name_and_units. # new dataset allocated
|
84
|
-
by_units_sold. # new dataset allocated
|
85
|
-
all
|
86
|
-
end
|
87
|
-
|
88
|
-
This is because any time a new, uncached dataset is returned by a dataset method, all subsequent methods in the method chain cannot benefit from caching.
|
89
|
-
|
90
|
-
Usually, when you are designing methods to process data based on user input, the user input affects the rows selected, and not the columns selected or the order in which the rows are returned. Sequel is aware of this and has dataset methods that specifically take user input (arguments), interpret them as a filter condition and either:
|
91
|
-
|
92
|
-
* Return all matching rows in an array (+where_all+)
|
93
|
-
* Iterate over all matching rows (+where_each+)
|
94
|
-
* Return first matching row (+first+)
|
95
|
-
* Return first column in first matching row, assumes only a single column is selected (+where_single_value+)
|
96
|
-
|
97
|
-
After calling these methods on a cached dataset a number of times (currently 3), Sequel will automatically build an optimized loader, cache it, and use it for future loads. So the above example changes to:
|
98
|
-
|
99
|
-
def all_albums_released_in_year(year)
|
100
|
-
with_name_and_units. # cached dataset returned
|
101
|
-
by_units_sold. # cached dataset returned
|
102
|
-
where_all(release_year: year) # cached loader used
|
103
|
-
end
|
104
|
-
|
105
|
-
This can significantly improve performance, up to 3x for complex method chains that only return a few rows.
|
106
|
-
|
107
|
-
So the general advice on designing dataset methods for performance is:
|
108
|
-
|
109
|
-
* Use +dataset_module+ methods to create named dataset methods that return cached datasets
|
110
|
-
* If any filtering is to be done, have it done last using +where_all+, +where_each+, +first+, or +where_single_value+.
|
111
|
-
|
112
|
-
By following this advice, you can significantly increase the performance of your model dataset code.
|
113
|
-
|
114
|
-
=== Further Increasing Performance
|
115
|
-
|
116
|
-
The best way to further increase performance at the Sequel level is to switch to using prepared statements. This does require more significant changes to the API. Here's an example using prepared statements:
|
117
|
-
|
118
|
-
class Album < Sequel::Model
|
119
|
-
ALBUMS_RELEASED_IN_YEAR = select(:id, :name, :copies_sold).
|
120
|
-
where(release_year: :$year).
|
121
|
-
order(Sequel.desc(:copies_sold)).
|
122
|
-
prepare(:all, :all_albums_released_in_year)
|
123
|
-
|
124
|
-
def self.all_albums_released_in_year(year)
|
125
|
-
ALBUMS_RELEASED_IN_YEAR.call(year: year)
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
Note that when using prepared statements, you need to use a class method instead of a dataset method, as the SQL for the prepared statement must be fixed for the class. This limits the flexibility of the method, since you can no longer call it on arbitrary datasets on the class.
|
data/doc/model_hooks.rdoc
DELETED
@@ -1,254 +0,0 @@
|
|
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 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. There are also around hooks for all types, which wrap the before hooks, the behavior, and the after hooks.
|
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 the plugins that ship with Sequel 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
|
-
* +around_validation+
|
27
|
-
* +before_validation+
|
28
|
-
* +validate+ method called
|
29
|
-
* +after_validation+
|
30
|
-
* +around_save+
|
31
|
-
* +before_save+
|
32
|
-
* +around_create+
|
33
|
-
* +before_create+
|
34
|
-
* INSERT QUERY
|
35
|
-
* +after_create+
|
36
|
-
* +after_save+
|
37
|
-
|
38
|
-
Sequel calls hooks in the following order when saving an existing object:
|
39
|
-
|
40
|
-
* +around_validation+
|
41
|
-
* +before_validation+
|
42
|
-
* +validate+ method called
|
43
|
-
* +after_validation+
|
44
|
-
* +around_save+
|
45
|
-
* +before_save+
|
46
|
-
* +around_update+
|
47
|
-
* +before_update+
|
48
|
-
* UPDATE QUERY
|
49
|
-
* +after_update+
|
50
|
-
* +after_save+
|
51
|
-
|
52
|
-
Note that all of the hook calls are the same, except that +around_create+, +before_create+ and +after_create+ are used for a new object, and +around_update+, +before_update+ and +after_update+ are used for an existing object. Note that +around_save+, +before_save+, and +after_save+ are called in both cases.
|
53
|
-
|
54
|
-
Note that the validation hooks are still called if <tt>validate: false</tt> option is passed to save. If you call <tt>Model#valid?</tt> manually, then only the validation hooks are called:
|
55
|
-
|
56
|
-
* +around_validation+
|
57
|
-
* +before_validation+
|
58
|
-
* +validate+ method called
|
59
|
-
* +after_validation+
|
60
|
-
|
61
|
-
Sequel calls hooks in the following order when destroying an existing object:
|
62
|
-
|
63
|
-
* +around_destroy+
|
64
|
-
* +before_destroy+
|
65
|
-
* DELETE QUERY
|
66
|
-
* +after_destroy+
|
67
|
-
|
68
|
-
Note that these hooks are only called when using <tt>Model#destroy</tt>, they are not called if you use <tt>Model#delete</tt>.
|
69
|
-
|
70
|
-
== Transaction-related Hooks
|
71
|
-
|
72
|
-
Sequel::Model no longer offers transaction hooks for model instances. However, you can use the database transaction hooks inside model +before_save+ and +after_save+ hooks:
|
73
|
-
|
74
|
-
class Album < Sequel::Model
|
75
|
-
def before_save
|
76
|
-
db.after_rollback{rollback_action}
|
77
|
-
super
|
78
|
-
end
|
79
|
-
|
80
|
-
def after_save
|
81
|
-
super
|
82
|
-
db.after_commit{commit_action}
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
== Running Hooks
|
87
|
-
|
88
|
-
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.
|
89
|
-
|
90
|
-
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:
|
91
|
-
|
92
|
-
Album.where(name: 'RF').update(copies_sold: Sequel.+(:copies_sold, 1))
|
93
|
-
# UPDATE albums SET copies_sold = copies_sold + 1 WHERE name = 'RF'
|
94
|
-
|
95
|
-
In this case, the +update+ method is called on the dataset returned by <tt>Album.where</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:
|
96
|
-
|
97
|
-
album = Album.first(name: 'RF')
|
98
|
-
album.update(copies_sold: album.copies_sold + 1)
|
99
|
-
# UPDATE albums SET copies_sold = 2 WHERE id = 1
|
100
|
-
|
101
|
-
For the destroy hooks, you need to make sure you call +destroy+ on the object:
|
102
|
-
|
103
|
-
album.destroy # runs destroy hooks
|
104
|
-
|
105
|
-
== Skipping Hooks
|
106
|
-
|
107
|
-
Sequel makes it easy to skip destroy hooks by calling +delete+ instead of +destroy+:
|
108
|
-
|
109
|
-
album.delete # does not run destroy hooks
|
110
|
-
|
111
|
-
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:
|
112
|
-
|
113
|
-
album.this # dataset
|
114
|
-
|
115
|
-
The +this+ dataset works just like any other dataset, so you can call +update+ on it to modify it:
|
116
|
-
|
117
|
-
album.this.update(copies_sold: album.copies_sold + 1)
|
118
|
-
|
119
|
-
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>:
|
120
|
-
|
121
|
-
Album.insert(name: 'RF') # does not run hooks
|
122
|
-
|
123
|
-
== Canceling Actions in Hooks
|
124
|
-
|
125
|
-
Sometimes want to cancel an action in a before hook, so the action is not performed. For example, you may want to not allow destroying or saving a record in certain cases. In those cases, you can call +cancel_action+ inside the <tt>before_*</tt> hook, which will stop processing the hook and will either raise a <tt>Sequel::HookFailed</tt> exception (the default), or return +nil+ if +raise_on_save_failure+ is +false+). You can use this to implement validation-like behavior, that will run even if validations are skipped:
|
126
|
-
|
127
|
-
class Album < Sequel::Model
|
128
|
-
def before_save
|
129
|
-
cancel_action if name == ''
|
130
|
-
super
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
|
-
For around hooks, neglecting to call +super+ halts hook processing in the same way as calling +cancel_action+ in a before hook. It's probably a bad idea to use +cancel_action+ hook processing in after hooks, or after yielding in around hooks, since by then the main processing has already taken place.
|
135
|
-
|
136
|
-
By default, Sequel runs hooks other than validation hooks inside a transaction, so if you cancel the action by calling +cancel_action+ in any 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 and the <tt>:transaction</tt> option passed to save).
|
137
|
-
|
138
|
-
== Conditional Hooks
|
139
|
-
|
140
|
-
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:
|
141
|
-
|
142
|
-
class Album < Sequel::Model
|
143
|
-
def before_update
|
144
|
-
self.timestamp ||= Time.now if status_id > 3
|
145
|
-
super
|
146
|
-
end
|
147
|
-
end
|
148
|
-
|
149
|
-
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.
|
150
|
-
|
151
|
-
== Using Hooks in Multiple Classes
|
152
|
-
|
153
|
-
If you want all your model classes to use the same hook, you can just define that hook in Sequel::Model:
|
154
|
-
|
155
|
-
class Sequel::Model
|
156
|
-
def before_create
|
157
|
-
self.created_at ||= Time.now
|
158
|
-
super
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
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.
|
163
|
-
|
164
|
-
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:
|
165
|
-
|
166
|
-
=== Plugin
|
167
|
-
|
168
|
-
module SetCreatedAt
|
169
|
-
module InstanceMethods
|
170
|
-
def before_create
|
171
|
-
self.created_at ||= Time.now
|
172
|
-
super
|
173
|
-
end
|
174
|
-
end
|
175
|
-
end
|
176
|
-
Album.plugin(SetCreatedAt)
|
177
|
-
Artist.plugin(SetCreatedAt)
|
178
|
-
|
179
|
-
=== Simple Module
|
180
|
-
|
181
|
-
module SetCreatedAt
|
182
|
-
def before_create
|
183
|
-
self.created_at ||= Time.now
|
184
|
-
super
|
185
|
-
end
|
186
|
-
end
|
187
|
-
Album.send(:include, SetCreatedAt)
|
188
|
-
Artist.send(:include, SetCreatedAt)
|
189
|
-
|
190
|
-
== +super+ Ordering
|
191
|
-
|
192
|
-
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:
|
193
|
-
|
194
|
-
class Album < Sequel::Model
|
195
|
-
def before_save
|
196
|
-
self.updated_at ||= Time.now
|
197
|
-
super
|
198
|
-
end
|
199
|
-
|
200
|
-
def after_save
|
201
|
-
super
|
202
|
-
AuditLog.create(log: "Album #{name} created")
|
203
|
-
end
|
204
|
-
end
|
205
|
-
|
206
|
-
This allows the following general principles to be true:
|
207
|
-
|
208
|
-
* before hooks are run in reverse order of inclusion
|
209
|
-
* after hooks are run in order of inclusion
|
210
|
-
|
211
|
-
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:
|
212
|
-
|
213
|
-
* model before hook
|
214
|
-
* plugin before hook
|
215
|
-
* plugin after hook
|
216
|
-
* model after hook
|
217
|
-
|
218
|
-
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).
|
219
|
-
|
220
|
-
== Around Hooks
|
221
|
-
|
222
|
-
Around hooks should only be used if you cannot accomplish the same results with before and after hooks. For example, if you want to catch database errors caused by the +INSERT+ or +UPDATE+ query when saving a model object and raise them as validation errors, you cannot use a before or after hook. You have use an +around_save+ hook:
|
223
|
-
|
224
|
-
class Album < Sequel::Model
|
225
|
-
def around_save
|
226
|
-
super
|
227
|
-
rescue Sequel::DatabaseError => e
|
228
|
-
# parse database error, set error on self, and reraise a Sequel::ValidationFailed
|
229
|
-
end
|
230
|
-
end
|
231
|
-
|
232
|
-
Likewise, let's say that upon retrieval, you associate an object with a file descriptor, and you want to ensure that the file descriptor is closed after the object is saved to the database. Let's assume you are always saving the object and you are not using validations. You could not use an +after_save+ hook safely, since if the database raises an error, the +after_save+ method will not be called. In this case, an +around_save+ hook is also the correct choice:
|
233
|
-
|
234
|
-
class Album < Sequel::Model
|
235
|
-
def around_save
|
236
|
-
super
|
237
|
-
ensure
|
238
|
-
@file_descriptor.close
|
239
|
-
end
|
240
|
-
end
|
241
|
-
|
242
|
-
== Hook related plugins
|
243
|
-
|
244
|
-
=== +instance_hooks+
|
245
|
-
|
246
|
-
Sequel also ships with an +instance_hooks+ plugin that allows you to define before and after hooks on a per instance basis. It's very useful as it allows you to delay action on an instance until before or after saving. This can be important if you want to modify a group of related objects together (which is how the +nested_attributes+ plugin uses +instance_hooks+).
|
247
|
-
|
248
|
-
=== +hook_class_methods+
|
249
|
-
|
250
|
-
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. However, it does not implement around hooks.
|
251
|
-
|
252
|
-
=== +after_initialize+
|
253
|
-
|
254
|
-
The after_initialize plugin adds an after_initialize hook, that is called for all model instances on creation (both new instances and instances retrieved from the database). It exists mostly for legacy compatibility, but it is still supported.
|
data/doc/model_plugins.rdoc
DELETED
@@ -1,270 +0,0 @@
|
|
1
|
-
= Model Plugins
|
2
|
-
|
3
|
-
Sequel::Model (and Sequel in general) is designed around the idea of a small core, to which application-specific behavior can easily be added. Sequel::Model implements this design using a plugin system. Plugins are modules that include submodules for model class methods, model instance methods, and model dataset methods. All plugins can override the class, instance, and dataset methods added by earlier plugins, and call super to get the behavior before the plugin was added.
|
4
|
-
|
5
|
-
== Default Plugins
|
6
|
-
|
7
|
-
The Sequel::Model class is completely empty by default, in that it has no class methods or instance methods. Sequel::Model is itself a plugin, and it is the first plugin loaded, and it is loaded into itself (meta!). So methods in Sequel::Model::ClassMethods become Sequel::Model class methods, methods in Sequel::Model::InstanceMethods become Sequel::Model instance methods, and methods in Sequel::Model::DatasetMethods become Sequel::Model dataset methods. The Sequel::Model plugin is often referred to as the base plugin.
|
8
|
-
|
9
|
-
By default, the Sequel::Model class also has the Sequel::Model::Associations plugin loaded by default, though it is possible to disable this.
|
10
|
-
|
11
|
-
== Loading Plugins
|
12
|
-
|
13
|
-
Loading a plugin into a model class is generally as simple as calling the Sequel::Model.plugin method with the name of the plugin, for example:
|
14
|
-
|
15
|
-
Sequel::Model.plugin :subclasses
|
16
|
-
|
17
|
-
What is does is require the <tt>sequel/plugins/subclasses</tt> file, and then assumes that that file defines the <tt>Sequel::Plugins::Subclasses</tt> plugin module.
|
18
|
-
|
19
|
-
It's possible to pass module instances to the plugin method to load plugins that are stored in arbitrary files or namespaces:
|
20
|
-
|
21
|
-
Sequel::Model.plugin MyApp::Plugins::Foo
|
22
|
-
|
23
|
-
In the examples shown above, the plugin is loaded into Sequel::Model, which means it is loaded into all subclasses that are created afterward. With many plugins, you are not going to want to add them to Sequel::Model, but to a specific subclass:
|
24
|
-
|
25
|
-
class Node < Sequel::Model
|
26
|
-
plugin :tree
|
27
|
-
end
|
28
|
-
|
29
|
-
Doing this, only Node and future subclasses of Node will have the tree plugin loaded.
|
30
|
-
|
31
|
-
== Plugin Arguments/Options
|
32
|
-
|
33
|
-
Some plugins require arguments and/or support options. For example, the single_table_inheritance plugin requires an argument containing the column that specifies the class to use, and options:
|
34
|
-
|
35
|
-
class Employee < Sequel::Model
|
36
|
-
plugin :single_table_inheritance, :type_id, model_map: {1=>:Staff, 2=>:Manager}
|
37
|
-
end
|
38
|
-
|
39
|
-
You should read the documentation for the plugin to determine if it requires arguments and what if any options are supported.
|
40
|
-
|
41
|
-
== Creating Plugins
|
42
|
-
|
43
|
-
The simplest possible plugin is an empty module in a file stored in <tt>sequel/plugins/plugin_name</tt> somewhere in ruby's load path:
|
44
|
-
|
45
|
-
module Sequel
|
46
|
-
module Plugins
|
47
|
-
module PluginName
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
Well, technically, that's not the simplest possible plugin, but it is the simplest one you can load by name. The absolute simplest plugin would be an empty module:
|
53
|
-
|
54
|
-
Sequel::Model.plugin Module.new
|
55
|
-
|
56
|
-
== Example Formatting
|
57
|
-
|
58
|
-
In general, loading plugins by module instead of by name is not recommended, so this guide will assume that plugins are loaded by name. For simplicity, we'll also use the following format for example plugin code (and assume a plugin named Foo stored in <tt>sequel/plugins/foo</tt>):
|
59
|
-
|
60
|
-
module Sequel::Plugins::Foo
|
61
|
-
end
|
62
|
-
|
63
|
-
This saves 4 lines per example. However, it's recommended that you use the nested example displayed earlier for production code.
|
64
|
-
|
65
|
-
The examples also assume that the following model class exists:
|
66
|
-
|
67
|
-
class Bar < Sequel::Model
|
68
|
-
end
|
69
|
-
|
70
|
-
== Adding Class Methods
|
71
|
-
|
72
|
-
If you want your plugin to add class methods to the model class it is loaded into, define a ClassMethods module under the plugin module:
|
73
|
-
|
74
|
-
module Sequel::Plugins::Foo
|
75
|
-
module ClassMethods
|
76
|
-
def a
|
77
|
-
1
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
This allows a plugin user to do:
|
83
|
-
|
84
|
-
Bar.plugin :foo
|
85
|
-
Bar.a # => 1
|
86
|
-
|
87
|
-
== Adding Instance Methods
|
88
|
-
|
89
|
-
If you want your plugin to add instance methods to the model class it is loaded into, define an InstanceMethods module under the plugin module:
|
90
|
-
|
91
|
-
module Sequel::Plugins::Foo
|
92
|
-
module InstanceMethods
|
93
|
-
def a
|
94
|
-
1
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
This allows a plugin user to do:
|
100
|
-
|
101
|
-
Bar.plugin :foo
|
102
|
-
Bar.new.a # => 1
|
103
|
-
|
104
|
-
== Adding Dataset Methods
|
105
|
-
|
106
|
-
If you want your plugin to add methods to the dataset of the model class it is loaded into, define a DatasetMethods module under the plugin module:
|
107
|
-
|
108
|
-
module Sequel::Plugins::Foo
|
109
|
-
module DatasetMethods
|
110
|
-
def a
|
111
|
-
1
|
112
|
-
end
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
This allows a plugin user to do:
|
117
|
-
|
118
|
-
Bar.plugin :foo
|
119
|
-
Bar.dataset.a # => 1
|
120
|
-
|
121
|
-
== Calling super to get Previous Behavior
|
122
|
-
|
123
|
-
No matter if you are dealing with class, instance, or dataset methods, you can call super inside the method to get the previous behavior. This makes it easy to hook into the method, add your own behavior, but still get the previous behavior:
|
124
|
-
|
125
|
-
module Sequel::Plugins::Foo
|
126
|
-
module InstanceMethods
|
127
|
-
def save
|
128
|
-
if allow_saving?
|
129
|
-
super
|
130
|
-
else
|
131
|
-
raise Sequel::Error, 'saving not allowed for this object'
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
private
|
136
|
-
|
137
|
-
def allow_saving?
|
138
|
-
moon =~ /Waxing/
|
139
|
-
end
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
== Running Code When the Plugin is Loaded
|
144
|
-
|
145
|
-
Some plugins require more than just adding methods. Any plugin that requires state is going to have to initialize that state and store it somewhere (generally in the model class itself). If you want to run code when a plugin is loaded (usually to initialize state, but possibly for other reasons), there are two methods you can define to do so. The first method is apply, and it is called only the first time the plugin is loaded into the class, before it is loaded into the class. This is generally only used if a plugin depends on another plugin or for initializing state. You define this method as a singleton method of the plugin module:
|
146
|
-
|
147
|
-
module Sequel::Plugins::Foo
|
148
|
-
def self.apply(model)
|
149
|
-
model.instance_eval do
|
150
|
-
plugin :plugin_that_foo_depends_on
|
151
|
-
@foo_states = {}
|
152
|
-
end
|
153
|
-
end
|
154
|
-
end
|
155
|
-
|
156
|
-
The other method is called configure, and it is called everytime the plugin is loaded into the class, after it is loaded into the class:
|
157
|
-
|
158
|
-
module Sequel::Plugins::Foo
|
159
|
-
def self.configure(model)
|
160
|
-
model.instance_eval do
|
161
|
-
@foo_states[:initial] ||= :baz
|
162
|
-
end
|
163
|
-
end
|
164
|
-
end
|
165
|
-
|
166
|
-
Note that in the configure method, you know apply has already been called at least once (so @foo_state will definitely exist).
|
167
|
-
|
168
|
-
If you want your plugin to take arguments and/or support options, you handle that by making your apply and configure methods take arguments and/or an options hash. For example, if you want the user to be able to set the initial state via an option, you can do:
|
169
|
-
|
170
|
-
module Sequel::Plugins::Foo
|
171
|
-
def self.apply(model, opts={})
|
172
|
-
model.instance_eval do
|
173
|
-
plugin :plugin_foo_depends_on
|
174
|
-
@foo_states = {}
|
175
|
-
end
|
176
|
-
end
|
177
|
-
|
178
|
-
def self.configure(model, opts={})
|
179
|
-
model.instance_eval do
|
180
|
-
@foo_states[:initial] = opts[:initial_state] || @foo_states[:initial] || :baz
|
181
|
-
end
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
|
-
This allows a user of the plugin to do either of the following
|
186
|
-
|
187
|
-
Bar.plugin :foo
|
188
|
-
Bar.plugin :foo, initial_state: :quux
|
189
|
-
|
190
|
-
If you want to require the initial state to be provided as an argument:
|
191
|
-
|
192
|
-
module Sequel::Plugins::Foo
|
193
|
-
def self.apply(model, initial_state)
|
194
|
-
model.instance_eval do
|
195
|
-
plugin :plugin_foo_depends_on
|
196
|
-
@foo_states = {}
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
def self.configure(model, initial_state)
|
201
|
-
model.instance_eval do
|
202
|
-
@foo_states[:initial] = initial_state
|
203
|
-
end
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
This requires that the user of the plugin specify the argument:
|
208
|
-
|
209
|
-
Bar.plugin :foo, :quux
|
210
|
-
|
211
|
-
In general you should only require plugin arguments if you absolutely must have a value and there is no good default.
|
212
|
-
|
213
|
-
== Handling Subclasses
|
214
|
-
|
215
|
-
Sequel::Model uses a copy-on-subclassing approach to model state. So instead of having a model subclass ask its superclass for a value if the subclass don't have the value defined, the value should be copied from the parent class to the subclass when the subclass is created. While this can be implemented by overriding the +inherited+ class method, there is an available shortcut that handles most cases:
|
216
|
-
|
217
|
-
module Sequel::Plugins::Foo
|
218
|
-
module ClassMethods
|
219
|
-
Sequel::Plugins.inherited_instance_variables(self, :@foo_states => :dup)
|
220
|
-
end
|
221
|
-
end
|
222
|
-
|
223
|
-
Inside the ClassMethods submodule, you call the Sequel::Plugins.inherited_instance_variables method with the first argument being self. The second argument should be a hash describing how to copy the value from the parent class into the subclass. The keys of this hash are instance variable names, including the @ symbol (e.g. :@foo_state). The values of this hash describe how to copy it:
|
224
|
-
|
225
|
-
nil :: Use the value directly.
|
226
|
-
:dup :: Call dup on the value.
|
227
|
-
:hash_dup :: Create a new hash with the same keys, but a dup of all the values.
|
228
|
-
Proc :: An arbitrary proc that is called with the parent class value and should return the value to set into the subclass.
|
229
|
-
|
230
|
-
== Handling Changes to the Model's Dataset
|
231
|
-
|
232
|
-
In many plugins, if the model class changes the dataset, you need to change the state for the plugin. While you can do this by overriding the set_dataset class method, there is an available shortcut:
|
233
|
-
|
234
|
-
module Sequel::Plugins::Foo
|
235
|
-
module ClassMethods
|
236
|
-
Sequel::Plugins.after_set_dataset(self, :set_foo_table)
|
237
|
-
|
238
|
-
private
|
239
|
-
|
240
|
-
def set_foo_table
|
241
|
-
@foo_states[:table] = table_name
|
242
|
-
end
|
243
|
-
end
|
244
|
-
end
|
245
|
-
|
246
|
-
With this code, any time the model's dataset changes, the state of the plugin will be updated to set the correct table name. This is also called when creating a new model class with a dataset.
|
247
|
-
|
248
|
-
== Making Dataset Methods Callable as Class Methods
|
249
|
-
|
250
|
-
In some cases, when dataset methods are added, you want to also create a model class method that will call the dataset method, so you can write:
|
251
|
-
|
252
|
-
Model.method
|
253
|
-
|
254
|
-
instead of:
|
255
|
-
|
256
|
-
Model.dataset.method
|
257
|
-
|
258
|
-
There is an available shortcut that automatically creates the class methods:
|
259
|
-
|
260
|
-
module Sequel::Plugins::Foo
|
261
|
-
module ClassMethods
|
262
|
-
Sequel::Plugins.def_dataset_methods(self, :quux)
|
263
|
-
end
|
264
|
-
|
265
|
-
module DatasetMethods
|
266
|
-
def quux
|
267
|
-
2
|
268
|
-
end
|
269
|
-
end
|
270
|
-
end
|