Swift и PostgreSQL: идеальное сочетание для backend в 2025

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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)                

Блоги

Рекомендаций по книгам