Connect with us

GitHub

SharingGRDB: быстрая, легкая замена SwiftData

Компания Point-Free выпустила значительное обновление библиотеки SharingGRDB, которая предлагает быструю, эргономичную и легкую замену SwiftData, работающую на базе SQL.

Опубликовано

/

     
     

Компания 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 непосредственно в виде строки, но при этом безопасным способом.

Важно

Хотя #sql дает вам возможность писать 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 вышел уже сегодня! Попробуйте и дайте нам знать, что вы думаете. Если у вас есть вопросы или комментарии, присоединяйтесь к нашим обсуждениям.

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: