Swift'ten MySQL'e bağlanmak

Selamlar, bu yazımda Swift dünyasından MySQL'e bağlanmaya bakacağız. Apple ekosisteminden gelenler için server-side Swift biraz uzak duran bir dünya gibi görünebiliyor; oysa Vapor olgun bir framework, Fluent de gayet iş gören bir ORM. Lafı uzatmadan başlayalım.

iOS uygulaması yazıyorsanız MySQL'e telefondan doğrudan bağlanmazsınız - araya bir backend koyarsınız. İşte o backend de pekâlâ Swift olabilir. Aynı dili hem istemci hem sunucu tarafında kullanmak ekip için epey rahatlatıcı, açıkçası ben de iki tarafı tek dilde tutmaktan yanayım.

mysql-nio ve FluentMySQLDriver

Swift'te MySQL'in standart yolu mysql-nio. Swift NIO üstüne kurulu, async ve non-blocking bir sürücü. Vapor projesinde ise FluentMySQLDriver paketi mysql-nio'yu Fluent ORM'e bağlıyor; çoğu zaman doğrudan ham sürücüye dokunmaya gerek kalmıyor.

Package.swift dosyamıza paketleri ekliyoruz:

let package = Package(
    name: "MyApp",
    platforms: [.macOS(.v13)],
    dependencies: [
        .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
        .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
        .package(url: "https://github.com/vapor/fluent-mysql-driver.git", from: "4.0.0"),
    ],
    targets: [
        .target(name: "App", dependencies: [
            .product(name: "Vapor", package: "vapor"),
            .product(name: "Fluent", package: "fluent"),
            .product(name: "FluentMySQLDriver", package: "fluent-mysql-driver"),
        ]),
    ]
)

Üç paket: Vapor, Fluent, ve MySQL sürücüsü. Bu kadar.

Bağlantıyı yapılandırmak

Vapor'da bağlantı configure.swift içinde tanımlanır. Burada kritik nokta şu: hostname, kullanıcı adı, şifre gibi şeyleri kod içine gömmüyoruz, Environment.get ile alıyoruz. Aksi halde repository'e sırrı commit'lersiniz, sonra da herkes silmek için git history'de boğuşur.

import Vapor
import Fluent
import FluentMySQLDriver

public func configure(_ app: Application) throws {
    app.databases.use(
        .mysql(
            hostname: Environment.get("DB_HOST") ?? "localhost",
            port: MySQLConfiguration.ianaPortNumber,
            username: Environment.get("DB_USER") ?? "app_user",
            password: Environment.get("DB_PASSWORD") ?? "secret",
            database: Environment.get("DB_NAME") ?? "shop",
            tlsConfiguration: .none
        ),
        as: .mysql
    )

    app.migrations.add(CreateProduct())
    try app.autoMigrate().wait()
}

tlsConfiguration: .none yerel geliştirmede tamam, ama prod'a aynısıyla çıkmayın. Production'da .forceTLS veya bir CA bundle ile kalkın - bence bu pazarlık konusu değil.

Model ve migration

Fluent'te model final class olarak Model ve Content protokollerini implement eder:

import Fluent
import Vapor

final class Product: Model, Content {
    static let schema = "products"

    @ID(key: .id)
    var id: UUID?

    @Field(key: "name")
    var name: String

    @Field(key: "price")
    var price: Double

    init() {}

    init(id: UUID? = nil, name: String, price: Double) {
        self.id = id
        self.name = name
        self.price = price
    }
}

Şemayı oluşturmak için bir AsyncMigration:

struct CreateProduct: AsyncMigration {
    func prepare(on database: Database) async throws {
        try await database.schema("products")
            .id()
            .field("name", .string, .required)
            .field("price", .double, .required)
            .create()
    }

    func revert(on database: Database) async throws {
        try await database.schema("products").delete()
    }
}

revert her zaman yazın, ben de ilk başlarda atlardım, sonra rollback gerekince pişman oluyordum.

Sorgu ve insert

Route handler içinde Fluent'in query builder'ı ile çalışıyoruz:

app.get("products") { req async throws -> [Product] in
    try await Product.query(on: req.db)
        .filter(\.$price < 50.0)
        .all()
}

app.post("products") { req async throws -> Product in
    let product = try req.content.decode(Product.self)
    try await product.save(on: req.db)
    return product
}

Tip-güvenli, async/await, fena değil.

Sık karşılaşılan tuzaklar

  • TLS'siz prod: tlsConfiguration: .none ile prod'a çıkmak. Trafik düz akar, kimse anlayana kadar geçmiş olsun.
  • autoMigrate().wait() bağımlılığı: Hızlı başlangıç için tamam ama büyüyen şemada migration'i CI adımına taşıyın.
  • Connection pool ayarsız: mysql-nio varsayılan pool'u küçük. Yük gelince timeout'lar tuhaflaşır; app.databases.use çağrısında maxConnectionsPerEventLoop ile düzgün ayarlayın.
  • iOS'tan doğrudan bağlanma denemesi: MySQL credential'larını uygulamaya gömmek - yapmayın. Araya HTTP API koyun.

Kapanış

Bu yazıda Swift tarafından MySQL'e Vapor + Fluent ile nasıl bağlandığımıza, modeli ve migration'i nasıl tanımladığımıza baktık. Bana sorarsanız Vapor projeleri için doğrudan FluentMySQLDriver yeter; mysql-nio'ya inmek ancak Vapor dışı bir CLI veya worker yazıyorsanız anlamlı. Umarım faydalı olur, bir sonraki yazıda görüşmek üzere.