Swift и PostgreSQL: почему это сочетание взрывает мозг
Когда мы говорим «серверный Swift», большинство разработчиков представляют себе Vapor, Perfect или Kitura. А когда речь заходит о базах данных — в голову приходит привычный MySQL или SQLite. Но давайте честно: если вы строите серьёзный backend на Swift, вам нужен PostgreSQL. Почему? Потому что это не просто база данных — это швейцарский нож для работы с данными, который идеально ложится на мощь Swift.
В этой статье мы разберём, как подружить Swift и PostgreSQL так, чтобы ваш код летал, база не падала, а коллеги завидовали. Никакой воды — только практика, код и реальные кейсы.
Почему PostgreSQL, а не MySQL?
Давайте сразу расставим точки над i. PostgreSQL — это объектно-реляционная СУБД, которая умеет то, о чём MySQL даже не мечтает:
- Нативные JSON и JSONB — храните сложные структуры прямо в таблицах
- Массивы — без костылей и дополнительных таблиц
- Параллельные запросы — используйте все ядра сервера
- Расширяемость — PostGIS, TimescaleDB, pgvector для AI
А главное — PostgreSQL идеально дружит с SwiftNIO (асинхронным движком, на котором построен Vapor). Это значит, что ваши запросы к базе не будут блокировать поток выполнения.
Установка и настройка драйвера PostgreSQL для Swift
Начнём с самого главного — как подключить PostgreSQL к вашему Swift-проекту. Мы будем использовать vapor/fluent-postgres-driver — самый популярный и стабильный драйвер.
Шаг 1: Добавляем зависимости
Откройте Package.swift и добавьте:
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "MySwiftBackend",
platforms: [
.macOS(.v13)
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "4.90.0"),
.package(url: "https://github.com/vapor/fluent.git", from: "4.10.0"),
.package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.8.0")
],
targets: [
.target(
name: "App",
dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "Fluent", package: "fluent"),
.product(name: "FluentPostgresDriver", package: "fluent-postgres-driver")
]
)
]
)
Шаг 2: Настраиваем подключение к базе
В файле configure.swift добавляем конфигурацию:
import Vapor
import Fluent
import FluentPostgresDriver
public func configure(_ app: Application) throws {
// Используем переменные окружения для безопасности
let hostname = Environment.get("DB_HOST") ?? "localhost"
let port = Environment.get("DB_PORT").flatMap(Int.init) ?? 5432
let username = Environment.get("DB_USERNAME") ?? "swift_user"
let password = Environment.get("DB_PASSWORD") ?? "swift_pass"
let database = Environment.get("DB_NAME") ?? "swift_backend"
app.databases.use(
.postgres(
hostname: hostname,
port: port,
username: username,
password: password,
database: database
),
as: .psql
)
// Регистрируем модели
app.migrations.add(CreateUser())
// Автомиграции при старте
try app.autoMigrate().wait()
}
Важный совет: никогда не хардкодьте пароли в коде. Используйте переменные окружения или .env файлы с помощью Vapor's Environment.
Создаём модель данных на Swift
Теперь самое интересное — определим модель пользователя. В Swift с Fluent это делается элегантно и лаконично:
import Fluent
import Vapor
final class User: Model, Content {
static let schema = "users"
@ID(key: .id)
var id: UUID?
@Field(key: "username")
var username: String
@Field(key: "email")
var email: String
@Field(key: "age")
var age: Int?
@Timestamp(key: "created_at", on: .create)
var createdAt: Date?
@Timestamp(key: "updated_at", on: .update)
var updatedAt: Date?
init() {}
init(id: UUID? = nil, username: String, email: String, age: Int?) {
self.id = id
self.username = username
self.email = email
self.age = age
}
}
// Миграция для создания таблицы
struct CreateUser: AsyncMigration {
func prepare(on database: Database) async throws {
try await database.schema("users")
.id()
.field("username", .string, .required)
.field("email", .string, .required)
.field("age", .int)
.field("created_at", .datetime)
.field("updated_at", .datetime)
.unique(on: "email")
.create()
}
func revert(on database: Database) async throws {
try await database.schema("users").delete()
}
}
Обратите внимание: мы используем @Timestamp — это автоматически заполняемые поля, за которые в MySQL пришлось бы писать триггеры.
CRUD операции: от простого к сложному
Теперь давайте реализуем полноценный API для работы с пользователями. Это будет классический REST с асинхронными запросами.
Создание пользователя (Create)
func createUser(req: Request) async throws -> User {
let user = try req.content.decode(User.self)
// Валидация на стороне сервера
guard user.email.contains("@") else {
throw Abort(.badRequest, reason: "Invalid email format")
}
try await user.save(on: req.db)
return user
}
Чтение с фильтрацией (Read)
func getUsers(req: Request) async throws -> [User] {
let minAge = req.query[Int.self, at: "min_age"] ?? 0
return try await User.query(on: req.db)
.filter(\.$age >= minAge)
.sort(\.$createdAt, .descending)
.all()
}
func getUserByID(req: Request) async throws -> User {
guard let userID = req.parameters.get("userID", as: UUID.self) else {
throw Abort(.badRequest)
}
guard let user = try await User.find(userID, on: req.db) else {
throw Abort(.notFound)
}
return user
}
Обновление (Update)
func updateUser(req: Request) async throws -> User {
let updatedData = try req.content.decode(User.self)
guard let userID = req.parameters.get("userID", as: UUID.self) else {
throw Abort(.badRequest)
}
guard let user = try await User.find(userID, on: req.db) else {
throw Abort(.notFound)
}
// Обновляем только переданные поля
user.username = updatedData.username
user.email = updatedData.email
user.age = updatedData.age
try await user.save(on: req.db)
return user
}
Удаление (Delete)
func deleteUser(req: Request) async throws -> HTTPStatus {
guard let userID = req.parameters.get("userID", as: UUID.self)