|
1 | 1 | Single-Row Tables
|
2 | 2 | =================
|
3 | 3 |
|
4 |
| -Let's talk about database tables that should contain a single row. |
5 |
| - |
6 |
| -Such tables can store configuration values, user preferences, and generally some global application state. They are a suitable alternative to `UserDefaults` in some applications, especially when configuration refers to values found in other database tables, and database integrity is a concern. |
7 |
| - |
8 |
| -An alternative way to store such configuration is a table of key-value pairs: two columns, and one row for each configuration value. This technique works, but it has a few drawbacks: you will have to deal with the various types of configuration values (strings, integers, dates, etc), and you won't be able to define foreign keys. This is why we won't explore key-value tables. |
9 |
| - |
10 |
| -This guide helps you implementing a single-row table with GRDB, with recommendations on the database schema, migrations, and the design of a matching [record] type. |
11 |
| - |
12 |
| -- [The Single-Row Table] |
13 |
| -- [The Single-Row Record] |
14 |
| -- [Wrap-Up] |
15 |
| - |
16 |
| - |
17 |
| -## The Single-Row Table |
18 |
| - |
19 |
| -As always with GRDB, everything starts at the level of the database schema. Putting the robust SQLite on our side is always a good idea, because we have to write less code, and this helps shipping less bugs. |
20 |
| - |
21 |
| -We want to instruct SQLite that our table must never contain more than one row. We will never have to wonder what to do if we were unlucky enough to find two rows with conflicting values in this table. |
22 |
| - |
23 |
| -SQLite is not able to guarantee that the table is never empty, so we have to deal with two cases: either the table is empty, or it contains one row. |
24 |
| - |
25 |
| -Those two cases can create a nagging question for the application. By default, inserts fail when the row already exists, and updates fail when the table is empty. In order to avoid those errors, we will have the app perform an insert in case of a failed update (in the [The Single-Row Record] chapter). And we instruct SQLite to just replace the eventual existing row in case of conflicting inserts: |
26 |
| - |
27 |
| -```swift |
28 |
| -// CREATE TABLE appConfiguration ( |
29 |
| -// id INTEGER PRIMARY KEY ON CONFLICT REPLACE CHECK (id = 1), |
30 |
| -// flag BOOLEAN NOT NULL, |
31 |
| -// ...) |
32 |
| -try db.create(table: "appConfiguration") { t in |
33 |
| - // Single row guarantee |
34 |
| - t.column("id", .integer) |
35 |
| - // Have inserts replace the existing row |
36 |
| - .primaryKey(onConflict: .replace) |
37 |
| - // Make sure the id column is always 1 |
38 |
| - .check { $0 == 1 } |
39 |
| - |
40 |
| - // The configuration columns |
41 |
| - t.column("flag", .boolean).notNull() |
42 |
| - // ... other columns |
43 |
| -} |
44 |
| -``` |
45 |
| - |
46 |
| -When you use [migrations], you may wonder if it is a good idea or not to perform an initial insert just after the table is created. Well, this is not recommended: |
47 |
| - |
48 |
| -```swift |
49 |
| -// NOT RECOMMENDED |
50 |
| -migrator.registerMigration("appConfiguration") { db in |
51 |
| - try db.create(table: "appConfiguration") { t in |
52 |
| - // Single row guarantee |
53 |
| - t.column("id", .integer).primaryKey(onConflict: .replace).check { $0 == 1 } |
54 |
| - |
55 |
| - // Define sensible defaults for each column |
56 |
| - t.column("flag", .boolean).notNull() |
57 |
| - .defaults(to: false) |
58 |
| - // ... other columns |
59 |
| - } |
60 |
| - |
61 |
| - // Populate the table |
62 |
| - try db.execute(sql: "INSERT INTO appConfiguration DEFAULT VALUES") |
63 |
| -} |
64 |
| -``` |
65 |
| - |
66 |
| -It is not a good idea to populate the table in a migration, for two reasons: |
67 |
| - |
68 |
| -1. This is not a hard guarantee that the table will never be empty. As a consequence, this won't prevent your Swift code from dealing with the possibility of a missing row. On top of that, the Swift code that deals with the missing row may not use the same default values as the SQLite schema (a [DRY] violation), with unclear consequences. |
69 |
| - |
70 |
| -2. Migrations that are shipped in the wild should *never* change, because you want to control the state of the databases installed on your users' devices, regardless of the application version they install first, regardless of how many application versions are skipped when they download an upgrade, etc. By inserting an initial row in a migration, you make it difficult for your application to adjust the sensible default values in the future, while keeping a clear idea of the various installation and upgrade scenarios. |
71 |
| - |
72 |
| -The recommended migration creates the table, nothing more: |
73 |
| - |
74 |
| -```swift |
75 |
| -// RECOMMENDED |
76 |
| -migrator.registerMigration("appConfiguration") { db in |
77 |
| - try db.create(table: "appConfiguration") { t in |
78 |
| - // Single row guarantee |
79 |
| - t.column("id", .integer).primaryKey(onConflict: .replace).check { $0 == 1 } |
80 |
| - |
81 |
| - // The configuration columns |
82 |
| - t.column("flag", .boolean).notNull() |
83 |
| - // ... other columns |
84 |
| - } |
85 |
| -} |
86 |
| -``` |
87 |
| - |
88 |
| - |
89 |
| -## The Single-Row Record |
90 |
| - |
91 |
| -Now that the database schema has been defined, we can define the [record] type that will help the application access the single row: |
92 |
| - |
93 |
| -```swift |
94 |
| -struct AppConfiguration: Codable { |
95 |
| - // Support for the single row guarantee |
96 |
| - private var id = 1 |
97 |
| - |
98 |
| - // The configuration properties |
99 |
| - var flag: Bool |
100 |
| - // ... other properties |
101 |
| -} |
102 |
| -``` |
103 |
| - |
104 |
| -In case the database table would be empty, we need a default configuration: |
105 |
| - |
106 |
| -```swift |
107 |
| -extension AppConfiguration { |
108 |
| - /// The default configuration |
109 |
| - static let `default` = AppConfiguration(flag: false, ...) |
110 |
| -} |
111 |
| -``` |
112 |
| - |
113 |
| -We make our record able to access the database: |
114 |
| - |
115 |
| -```swift |
116 |
| -extension AppConfiguration: FetchableRecord, PersistableRecord { |
117 |
| -``` |
118 |
| - |
119 |
| -We have seen in the [The Single-Row Table] chapter that by default, updates throw an error if the database table is empty. To avoid this error, we instruct GRDB to insert the missing default configuration before attempting to update (see [Persistence Callbacks] for more information about the `willSave` method): |
120 |
| - |
121 |
| -```swift |
122 |
| - // Customize the default PersistableRecord behavior |
123 |
| - func willUpdate(_ db: Database, columns: Set<String>) throws { |
124 |
| - // Insert the default configuration if it does not exist yet. |
125 |
| - if try !exists(db) { |
126 |
| - try AppConfiguration.default.insert(db) |
127 |
| - } |
128 |
| - } |
129 |
| -``` |
130 |
| - |
131 |
| -The standard GRDB method `fetchOne` returns an optional which is nil when the database table is empty. As a convenience, let's define a method that returns a non-optional (replacing the missing row with `default`): |
132 |
| - |
133 |
| -```swift |
134 |
| - /// Returns the persisted configuration, or the default one if the |
135 |
| - /// database table is empty. |
136 |
| - static func fetch(_ db: Database) throws -> AppConfiguration { |
137 |
| - try fetchOne(db) ?? .default |
138 |
| - } |
139 |
| -} |
140 |
| -``` |
141 |
| - |
142 |
| -And that's it! Now we can use our singleton record: |
143 |
| - |
144 |
| -```swift |
145 |
| -// READ |
146 |
| -let config = try dbQueue.read { db in |
147 |
| - try AppConfiguration.fetch(db) |
148 |
| -} |
149 |
| -if config.flag { |
150 |
| - // ... |
151 |
| -} |
152 |
| - |
153 |
| -// WRITE |
154 |
| -try dbQueue.write { db in |
155 |
| - var config = try AppConfiguration.fetch(db) |
156 |
| - |
157 |
| - // Update some config values |
158 |
| - try config.updateChanges(db) { |
159 |
| - $0.flag = true |
160 |
| - } |
161 |
| - |
162 |
| - // Other possible ways to write config: |
163 |
| - try config.update(db) |
164 |
| - try config.save(db) |
165 |
| - try config.insert(db) |
166 |
| - try config.upsert(db) |
167 |
| -} |
168 |
| -``` |
169 |
| - |
170 |
| -The four `update`, `save`, `insert` and `upsert` methods can be used interchangeably. They all make sure the configuration is stored in the database. |
171 |
| - |
172 |
| -The `updateChanges` method only updates the values changed by its closure argument (and performs an initial insert of default configuration if the database table is empty). |
173 |
| - |
174 |
| -See [Persistence Methods] for more information. |
175 |
| - |
176 |
| - |
177 |
| -## Wrap-Up |
178 |
| - |
179 |
| -We all love to copy and paste, don't we? Just customize the template code below: |
180 |
| - |
181 |
| -```swift |
182 |
| -// Table creation |
183 |
| -try db.create(table: "appConfiguration") { t in |
184 |
| - // Single row guarantee |
185 |
| - t.column("id", .integer).primaryKey(onConflict: .replace).check { $0 == 1 } |
186 |
| - |
187 |
| - // The configuration columns |
188 |
| - t.column("flag", .boolean).notNull() |
189 |
| - // ... other columns |
190 |
| -} |
191 |
| -``` |
192 |
| - |
193 |
| -```swift |
194 |
| -// |
195 |
| -// AppConfiguration.swift |
196 |
| -// |
197 |
| - |
198 |
| -import GRDB |
199 |
| - |
200 |
| -struct AppConfiguration: Codable { |
201 |
| - // Support for the single row guarantee |
202 |
| - private var id = 1 |
203 |
| - |
204 |
| - // The configuration properties |
205 |
| - var flag: Bool |
206 |
| - // ... other properties |
207 |
| -} |
208 |
| - |
209 |
| -extension AppConfiguration { |
210 |
| - /// The default configuration |
211 |
| - static let `default` = AppConfiguration(flag: false, ...) |
212 |
| -} |
213 |
| - |
214 |
| -// Database Access |
215 |
| -extension AppConfiguration: FetchableRecord, PersistableRecord { |
216 |
| - // Customize the default PersistableRecord behavior |
217 |
| - func willUpdate(_ db: Database, columns: Set<String>) throws { |
218 |
| - // Insert the default configuration if it does not exist yet. |
219 |
| - if try !exists(db) { |
220 |
| - try AppConfiguration.default.insert(db) |
221 |
| - } |
222 |
| - } |
223 |
| - |
224 |
| - /// Returns the persisted configuration, or the default one if the |
225 |
| - /// database table is empty. |
226 |
| - static func fetch(_ db: Database) throws -> AppConfiguration { |
227 |
| - try fetchOne(db) ?? .default |
228 |
| - } |
229 |
| -} |
230 |
| -``` |
231 |
| - |
232 |
| - |
233 |
| -[migrations]: https://groue.github.io/GRDB.swift/docs/6.3/documentation/grdb/migrations |
234 |
| -[record]: ../README.md#records |
235 |
| -[The Single-Row Table]: #the-single-row-table |
236 |
| -[The Single-Row Record]: #the-single-row-record |
237 |
| -[Wrap-Up]: #wrap-up |
238 |
| -[DRY]: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself |
239 |
| -[Persistence Callbacks]: ../README.md#persistence-callbacks |
240 |
| -[Persistence Methods]: ../README.md#persistence-methods |
| 4 | +This guide [has moved](https://groue.github.io/GRDB.swift/docs/6.3/documentation/grdb/singlerowtables). |
0 commit comments