Компания Point-Free выпустила значительное обновление библиотеки SharingGRDB, которая предлагает быструю, эргономичную и легкую замену SwiftData, работающую на базе SQL. Она предоставляет API, аналогичные @Model, @Query и #Predicate, но настроена на прямой доступ к базовой базе данных (то, от чего абстрагируется SwiftData), что дает вам больше возможностей, больше гибкости и больше производительности при сохранении и получении данных в приложении.
Макрос @Table
Основным новшеством, используемым библиотекой, является новый макрос @Table, который открывает богатый, безопасный для типов язык построения запросов, а также высокопроизводительный декодер для преобразования примитивов базы данных в первоклассные типы данных Swift. Он служит целям, аналогичным (и по синтаксису) макросу @Model в SwiftData:
SharingGRDB
@Table
struct Reminder {
let id: Int
var title = ""
var isCompleted = false
}
SwiftData
@Model
class Reminder {
var title: String
var isCompleted: Bool
init(
title: String = "",
isCompleted: Bool = false
) {
self.title = title
self.isCompleted = isCompleted
}
}
Некоторые ключевые различия:
- Макрос
@Tableработает со структурами, в то время как@Modelработает только с классами. - Поскольку
@Model-версияReminderявляется классом, необходимо предоставить инициализатор. - Версии
Reminderс@Modelне нужно полеid, поскольку SwiftData предоставляет постоянный идентификатор (persistentIdentifier) каждой модели.
Применив @Table, Reminder получает мгновенный доступ к мощному набору API для построения запросов, который позволяет создавать различные запросы с помощью выразительного Swift, подобно тому, как SwiftData использует #Predicate и ключевые пути в макросе @Query:
SharingGRDB
@FetchAll(
Reminder.where {
$0.title.contains("get")
&& !$0.isCompleted
}
.order(by: \.title)
)
var reminders
SwiftData
@Query(
filter: #Predicate<Reminder> {
$0.title.contains("get")
&& !$0.isCompleted
},
sort: \Reminder.title
)
var reminders: [Reminder]
Оба вышеприведенных примера получают элементы из внешнего хранилища данных, используя типы данных Swift, и оба автоматически наблюдаются SwiftUI, чтобы представления пересчитывались при изменении внешних данных, но SharingGRDB можно использовать и за пределами представления: в моделях @Observable, контроллерах представления UIKit и других.
Конструктор запросов отображает синтаксически правильный SQL, поэтому вы можете быть уверены, что он будет работать еще во время компиляции. Между тем, #Predicate вполне можно использовать таким образом, что в лучшем случае это приведет к загадочным ошибкам при компиляции, а в худшем — к краху во время выполнения.
Например, использование в запросе вычисляемого, а не хранимого свойства — это ошибка компилятора в SharingGRDB, но сбой во время выполнения в SwiftData:
SharingGRDB
@FetchAll(
Reminder.where {
// 'Reminder.TableColumns' has
// no member 'isNotCompleted'
$0.isNotCompleted
}
.order(by: \.title)
)
var reminders
SwiftData
@Query(
filter: #Predicate<Reminder> {
// Fatal error: Couldn't find
// 'isNotCompleted' on Reminder
$0.isNotCompleted
},
sort: \Reminder.title
)
var reminders: [Reminder]
Конструктор запросов также открывает перед вами весь спектр возможностей SQL, в то время как SwiftData скрывает эти детали и вместо этого предоставляет свой собственный язык построения запросов, который может выполнять только часть задач, которые может выполнять SQL.
Все, что можно сделать с помощью SwiftData, и даже больше, можно сделать с помощью SharingGRDB. Подробнее см. в разделе «Сравнение со SwiftData«.
Безопасные строки SQL
Мы никогда не хотели, чтобы наш конструктор запросов мешал написанию конкретного запроса. Поэтому мы создали макрос #sql, который позволяет отказаться от синтаксиса построителя запросов и писать SQL непосредственно в виде строки, но при этом безопасным способом.
В качестве простого примера можно выбрать заголовки из всех напоминаний следующим образом:
@FetchAll(
#sql("SELECT title FROM reminders", as: String.self)
)
var reminderTitles
Также можно сохранить безопасность схемы при написании SQL в виде строки. Вы можете использовать интерполяцию строк вместе со статическим описанием вашей схемы, предоставляемым @Table, чтобы ссылаться на ее столбцы и имя таблицы:
@FetchAll(
#sql("SELECT \(Reminder.title) FROM \(Reminder.self)", as: String.self)
)
var reminderTitles
Это генерирует тот же запрос, что и раньше, но теперь у вас есть больше статической безопасности при обращении к именам столбцов и таблиц в ваших типах.
Вы даже можете выбрать все столбцы из таблицы напоминаний, используя статическое свойство columns:
@FetchAll(
#sql("SELECT \(Reminder.columns) FROM \(Reminder.self)", as: Reminder.self)
)
var reminders
Обратите внимание, что это позволяет теперь декодировать результат в полный тип Reminder.
Макрос #sql также можно использовать для введения строк SQL в конструктор запросов с выбранной вами степенью детализации:
let searchTerm = "order%"
@FetchAll(
Reminder
.where {
#sql("\($0.title) COLLATE NOCASE NOT LIKE \(bind: searchTerm)")
}
.order(by: \.title)
)
var reminders
Но это лишь поверхностный обзор. Макрос #sql также выполняет базовую проверку строк SQL, чтобы выявить синтаксические ошибки во время компиляции. Дополнительные сведения см. в разделе «Безопасные SQL-строки«.
Производительность
SharingGRDB использует высокопроизводительное декодирование для преобразования полученных данных в ваши доменные типы Swift и имеет профиль производительности, аналогичный прямому вызову SQLite C API.
Ниже приведены бенчмарки с набором тестов производительности Lighter, чтобы увидеть, как это сравнивается:
Orders.fetchAll setup rampup duration SQLite (generated by Enlighter 1.4.10) 0 0.144 7.183 Lighter (1.4.10) 0 0.164 8.059 SharingGRDB (0.2.0) 0 0.172 8.511 GRDB (7.4.1, manual decoding) 0 0.376 18.819 SQLite.swift (0.15.3, manual decoding) 0 0.564 27.994 SQLite.swift (0.15.3, Codable) 0 0.863 43.261 GRDB (7.4.1, Codable) 0.002 1.07 53.326
Стало возможным благодаря StructuredQueries
Причина, по которой нам удалось добиться значительных успехов в эргономике и производительности SharingGRDB, кроется в еще одной библиотеке, которую мы выпускаем сегодня: StructuredQueries. Она предоставляет набор инструментов, позволяющих писать безопасный, выразительный, композабл SQL с помощью Swift, включая макрос @Table и его API для построения запросов, макрос #sql и многое другое.
Вы просто прикрепляете макросы к типам, которые представляют схему вашей базы данных. Развивая предыдущий пример:
@Table
struct Reminder {
let id: Int
var title = ""
var isCompleted = false
var priority: Priority?
@Column(as: Date.ISO8601Representation?.self)
var dueDate: Date?
}
Он предоставляет широкий набор API для создания запросов, начиная от простых:
Swift
Reminder.all // => [Reminder]
SQL
SELECT "reminders"."id", "reminders"."title", "reminders"."isCompleted", "reminders"."priority", "reminders"."dueDate" FROM "reminders"
И до сложных:
Swift
Reminder
.select {
($0.priority,
$0.title.groupConcat())
}
.where { !$0.isCompleted }
.group(by: \.priority)
.order { $0.priority.desc() }
// => [(Priority?, String)]
SQL
SELECT
"reminders"."priority",
group_concat("reminders"."title")
FROM "reminders"
WHERE (NOT "reminders"."isCompleted")
GROUP BY "reminders"."priority"
ORDER BY "reminders"."priority" DESC
Эти API помогают вам избежать проблем, связанных с опечатками и ошибками типов, но при этом они по-прежнему воспринимают SQL таким, какой он есть. StructuredQueries — это не ORM и не новый язык запросов, который вам придется изучать: ее API разработаны так, чтобы читать SQL, который она генерирует, хотя он часто более лаконичный и всегда более безопасный.
Библиотека поддерживает построение всего: от операторов SELECT, INSERT, UPDATE и DELETE до безопасных для типов внешних объединений и рекурсивных выражений общей таблицы. Чтобы узнать больше о построении SQL с помощью StructuredQueries, ознакомьтесь с документацией.
И хотя релиз StructuredQueries ориентирован на SQLite и, в частности, на его драйвер SharingGRDB, библиотека является универсальной, и ее построитель и декодер запросов может взаимодействовать с другими базами данных (MySQL, Postgres и т.д.) и библиотеками баз данных.
Если вы заинтересованы в создании интеграции StructuredQueries с другой библиотекой баз данных, пожалуйста, начните обсуждение и сообщите нам о любых проблемах, с которыми вы столкнетесь.
Попробуйте уже сегодня
Релиз 0.2.0 SharingGRDB вышел уже сегодня! Попробуйте и дайте нам знать, что вы думаете. Если у вас есть вопросы или комментарии, присоединяйтесь к нашим обсуждениям.

