Skip to content

Commit 9d03c1a

Browse files
committed
DocC: Single-Row Tables
1 parent 3014ddb commit 9d03c1a

File tree

6 files changed

+234
-241
lines changed

6 files changed

+234
-241
lines changed

Documentation/GRDB6MigrationGuide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ The record protocols have been refactored. We tried to keep the amount of modifi
195195

196196
To help you update your applications with persistence callbacks, let's look at two examples.
197197

198-
First, check the updated [Single-Row Tables](SingleRowTables.md) guide, if your application defines a "singleton record".
198+
First, check the updated [Single-Row Tables](https://groue.github.io/GRDB.swift/docs/6.3/documentation/grdb/singlerowtables) guide, if your application defines a "singleton record".
199199

200200
Next, let's consider a record that performs some validation before insertion and updates. In GRDB 5, this would look like:
201201

Documentation/GoodPracticesForDesigningRecordTypes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ Let's look at three examples:
245245
246246
**Singleton Records** are records that store configuration values, user preferences, and generally some global application state. They are backed by a database table that contains a single row.
247247
248-
The recommended setup for such records is described in the [Single-Row Tables](SingleRowTables.md) guide. Go check it, and come back when you're done!
248+
The recommended setup for such records is described in the [Single-Row Tables](https://groue.github.io/GRDB.swift/docs/6.3/documentation/grdb/singlerowtables) guide. Go check it, and come back when you're done!
249249
250250
251251
## Define Record Requests

Documentation/SingleRowTables.md

Lines changed: 1 addition & 237 deletions
Original file line numberDiff line numberDiff line change
@@ -1,240 +1,4 @@
11
Single-Row Tables
22
=================
33

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).

GRDB/Documentation.docc/Documentation.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ GRDB provides raw access to SQL and advanced SQLite features, because one someti
3636
### Database Techniques
3737

3838
- <doc:DatabaseSharing>
39+
- <doc:SingleRowTables>
3940

4041
### Combine Publishers
4142

GRDB/Documentation.docc/Migrations.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,13 +142,13 @@ It is much easier to control the schema of all databases deployed on users' devi
142142

143143
```swift
144144
migrator.registerMigration("Create authors") { db in
145-
// 👍 RECOMMENDED
145+
// RECOMMENDED
146146
try db.create(table: "author") { t in
147147
t.autoIncrementedPrimaryKey("id")
148148
...
149149
}
150150

151-
// 👎 NOT RECOMMENDED
151+
// NOT RECOMMENDED
152152
try db.create(table: Author.databaseTableName) { t in
153153
t.autoIncrementedPrimaryKey(Author.Columns.id.name)
154154
...

0 commit comments

Comments
 (0)