-
Notifications
You must be signed in to change notification settings - Fork 65
/
deleting_records.cr
325 lines (236 loc) · 9.37 KB
/
deleting_records.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
class Guides::Database::DeletingRecords < GuideAction
ANCHOR_SOFT_DELETE = "perma-soft-delete"
guide_route "/database/deleting-records"
def self.title
"Deleting records"
end
def markdown : String
<<-MD
## Delete Operations
Similar to the `SaveOperation`, Avram comes with a `DeleteOperation` that's generated with each model.
This allows you to write more complex logic around deleteing records. (e.g. delete confirmations, etc...)
### Simple deletes
If you just want to delete a record without any validations or callbacks, the simplest way is to use the generated `{ModelName}::DeleteOperation`
For example, if we have a `Server` model, Lucky will generate a `Server::DeleteOperation` that you can use:
```crystal
server = ServerQuery.find(123)
Server::DeleteOperation.delete!(server)
```
If the record fails to be deleted, an `Avram::InvalidOperationError` will be raised.
### Setting up a custom DeleteOperation
You can customize DeleteOperations with callbacks and validations. These
classes go in your `src/operations/` directory, and will inherit from
`{ModelName}::DeleteOperation`.
```crystal
# src/operations/delete_server.cr
class DeleteServer < Server::DeleteOperation
end
```
### Using a `DeleteOperation` in actions
The interface should feel pretty familiar. The object being deleted is passed in to the `delete` method, and a block will
return the operation instance, and the object being deleted.
```crystal
# src/actions/servers/delete.cr
class Servers::Delete < BrowserAction
delete "/servers/:server_id" do
server = ServerQuery.find(server_id)
DeleteServer.delete(server) do |operation, deleted_server|
if operation.deleted?
redirect to: Servers::Index
else
flash.failure = "Could not delete"
html Servers::EditPage, server: deleted_server
end
end
end
end
```
You can also pass in params or named args for use with attributes, or `needs`.
```crystal
DeleteServer.delete(server, params, secret_codes: [23_u16, 94_u16]) do |operation, deleted_server|
if operation.deleted?
redirect to: Servers::Index
else
flash.failure = "Could not delete"
html Servers::EditPage, server: deleted_server
end
end
```
### Delete and raise if it fails
You can also use the `delete!` method if you don't need validations and expect deletes to work every time:
```crystal
DeleteServer.delete!(server)
```
This is helpful when your operation only has callbacks or needs and is expected to work every time.
## `DeleteOperation` Callbacks and Validations
DeleteOperations come with `before_delete` and `after_delete` callbacks that allow you to either validate
some code before performing the delete, or perform some action after deleteing. (i.e. Send a "Goodbye" email, etc...)
Along with the callbacks, you also have access to `attribute`, `needs`, and all of the columns related to a model.
You even have `file_attribute` for those times you need to use biometric scans to authorize deleting a record!
### before_delete
```crystal
# src/operations/delete_server.cr
class DeleteServer < Server::DeleteOperation
attribute confirmation : String
before_delete do
validate_required confirmation
# `record` is the object to be deleted
if confirmation.value != record.server_name
confirmation.add_error("Confirmation must match the server name")
end
end
end
```
### after_delete
```crystal
# src/operations/delete_server.cr
class DeleteServer < Server::DeleteOperation
needs secret_codes : Array(UInt16)
after_delete do |deleted_server|
decrypted_server_data = DecryptServer.new(deleted_server, with: secret_codes)
DecryptedServerDataEmail.new(decrypted_server_data).deliver
end
end
```
### Bulk delete
> Currently bulk deletes with DeleteOperation are not supported.
If you need to bulk delete a group of records based on a where query, you can use `delete` at the end of your query.
This returns the number of records deleted.
```crystal
# DELETE FROM users WHERE banned_at IS NOT NULL
UserQuery.new.banned_at.is_not_nil.delete
```
#{permalink(ANCHOR_SOFT_DELETE)}
## Soft Deletes
A "soft delete" is when you want to hide a record as if it were deleted, but you want to keep the actual
record in your database. This allows you to restore the record without losing any previous data or associations.
Avram comes with some built-in modules to help make working with soft deleted records a lot easier. Let's add it
to an existing `Article` model.
* First, we need to add a new `soft_deleted_at : Time?` column to the table that needs soft deletes.
```
# Run this in your terminal
lucky gen.migration AddSoftDeleteToArticles
```
* Open your new `db/migrations/#{Time.utc.to_s("%Y%m%d%H%I%S")}_add_soft_delete_to_articles.cr` file.
```crystal
def migrate
alter table_for(Article) do
add soft_deleted_at : Time?, index: true
end
end
```
* Now open your `src/models/article.cr` file.
```crystal
class Article < BaseModel
# Include this module to add methods for
# soft deleting and restoring
include Avram::SoftDelete::Model
table do
# Add the new column to your model
column soft_deleted_at : Time?
end
end
```
* Next you need to update `src/queries/article_query.cr`.
```crystal
class ArticleQuery < Article::BaseQuery
# Include this module to add methods for
# querying and soft deleting records
include Avram::SoftDelete::Query
end
```
### Marking a record as soft deleted
Once a model includes the `Avram::SoftDelete::Model`, the associated DeleteOperation will handle the soft delete for you.
```crystal
# src/operations/delete_article.cr
class DeleteArticle < Article::DeleteOperation
end
```
and in your action
```crystal
# src/actions/articles/delete.cr
class Articles::Delete < BrowserAction
delete "/articles/:article_id" do
article = ArticleQuery.find(article_id)
deleted_article = DeleteArticle.delete!(article)
# This returns `true`
deleted_article.soft_deleted?
redirect to: Articles::Index
end
end
```
### Soft deleting in bulk
> Currently bulk soft deletes with DeleteOperation are not supported.
You can bulk update a group of records as soft deleted with the `soft_delete` method on your Query object.
```crystal
articles_to_delete = ArticleQuery.new.created_at.gt(3.years.ago)
# Marks the articles created over 3 years ago as soft deleted
articles_to_delete.soft_delete
```
### Restore a soft deleted record
If you need to restore a soft deleted record, you can use the `restore` method on the model instance.
```crystal
# Set the `soft_deleted_at` back to `nil`
article.restore
```
### Bulk restoring soft deleted records
The same as we can bulk soft delete records, we can also bulk update to restore them with
the `restore` method on your Query object.
```crystal
articles_to_restore = ArticleQuery.new.published_at.lt(1.week.ago)
# Restore recently published articles
articles_to_restore.restore
```
### Query soft deleted records
```crystal
# Return all articles that are not soft deleted
ArticleQuery.new.only_kept
# Return all articles that are soft deleted
ArticleQuery.new.only_soft_deleted
```
### Default queries without soft deleted
If you want to filter out soft deleted records by default, it's really easy to do.
Just add the `only_kept` method as the default query in the `initialize` method.
```crystal
class ArticleQuery < Article::BaseQuery
include Avram::SoftDelete::Query
# All queries will scope to only_kept
def initialize
defaults &.only_kept
end
end
```
```crystal
# Return all articles that are not soft deleted
ArticleQuery.new
```
Even with your default scope, you can still return soft deleted records when you need.
```crystal
# Return all articles, both `kept` and soft deleted
ArticleQuery.new.with_soft_deleted
```
## Truncating
### Truncate table
If you need to delete every record in the entire table, you can use `truncate`.
`TRUNCATE TABLE users`
```crystal
UserQuery.truncate
```
> Running the `truncate` method may raise an error similar to the following:
>
> `Error message cannot truncate a table referenced in a foreign key constraint.`
>
> If that's the case, call the same method with the `cascade` option set to `true`:
>
> `UserQuery.truncate(cascade: true)`
>
> This will automatically delete or update matching records in a child table where a foreign key relationship is in place.
### Truncate database
You can also truncate your entire database by calling `truncate` on your database class.
```crystal
AppDatabase.truncate
```
> This method is great for tests; horrible for production. Also note this method is not chainable.
MD
end
end