Go / database/sql package

database/sql package

Ихэнх бодит программ өгөгдлийг хадгалдаг — хэрэглэгчид, захиалгууд, нийтлэлүүд. Эдгээр өгөгдлийг хадгалахад database ашигладаг. Go-д database/sql гэсэн стандарт package байдаг бөгөөд энэ нь PostgreSQL, MySQL, SQLite зэрэг бүх SQL database-тэй нэгдсэн интерфэйсээр ажиллах боломжийг олгодог. Энэ хичээлд SQLite ашиглан суурь ойлголтуудыг судална.

database/sql-ийг эхлүүлэх

database/sql нь зөвхөн интерфэйс тодорхойлдог. Тодорхой database-тэй ажиллахын тулд driver суулгах хэрэгтэй. SQLite-д modernc.org/sqlite driver ашиглана:

bash
go get modernc.org/sqlite
go
package main

import (
    "database/sql"
    "fmt"
    "log"

    _ "modernc.org/sqlite" // driver-ийг бүртгэх, гэхдээ шууд ашиглахгүй
)

func main() {
    // Database нээх (файл байхгүй бол үүснэ)
    db, err := sql.Open("sqlite", "myapp.db")
    if err != nil {
        log.Fatal("Database нээхэд алдаа:", err)
    }
    defer db.Close()

    // Холболт амжилттай эсэхийг шалгах
    if err := db.Ping(); err != nil {
        log.Fatal("Database холбогдохгүй байна:", err)
    }

    fmt.Println("Database-тэй амжилттай холбогдлоо!")
}

_ ашиглан driver import хийх нь Go-д өргөн хэрэглэгддэг — package-ийн init() функцийг ажиллуулж driver-ийг бүртгэдэг.

Хүснэгт үүсгэх ба өгөгдөл оруулах

db.Exec() нь өгөгдөл буцаадаггүй SQL команд (CREATE, INSERT, UPDATE, DELETE) ажиллуулахад хэрэглэгддэг:

go
func хүснэгтҮүсгэх(db *sql.DB) error {
    query := `
        CREATE TABLE IF NOT EXISTS хэрэглэгчид (
            id      INTEGER PRIMARY KEY AUTOINCREMENT,
            нэр     TEXT    NOT NULL,
            и_мэйл TEXT    UNIQUE NOT NULL,
            xp      INTEGER DEFAULT 0
        )
    `
    _, err := db.Exec(query)
    return err
}

func хэрэглэгчНэмэх(db *sql.DB, нэр, иМэйл string) (int64, error) {
    // ? тэмдэг нь SQL injection-оос хамгаалдаг — заавал ашигла!
    result, err := db.Exec(
        "INSERT INTO хэрэглэгчид (нэр, и_мэйл) VALUES (?, ?)",
        нэр, иМэйл,
    )
    if err != nil {
        return 0, err
    }

    // Шинээр үүссэн мөрийн ID авах
    id, err := result.LastInsertId()
    return id, err
}

func main() {
    db, _ := sql.Open("sqlite", "myapp.db")
    defer db.Close()

    хүснэгтҮүсгэх(db)

    id, err := хэрэглэгчНэмэх(db, "Болд", "bold@example.mn")
    if err != nil {
        log.Println("Алдаа:", err)
        return
    }
    fmt.Printf("Шинэ хэрэглэгч нэмэгдлээ, ID: %d\n", id)
}

Чухал: SQL query-д утгыг шууд string болгон бүү нэгтгэ — ? placeholder ашигла. Энэ нь SQL injection халдлагаас хамгаалдаг.

Өгөгдөл унших — Query ба Scan

Өгөгдөл уншихад db.Query() болон db.QueryRow() ашигладаг:

go
type Хэрэглэгч struct {
    ID     int
    Нэр    string
    ИМэйл  string
    XP     int
}

// Бүх хэрэглэгчдийг авах
func бүхХэрэглэгчид(db *sql.DB) ([]Хэрэглэгч, error) {
    rows, err := db.Query("SELECT id, нэр, и_мэйл, xp FROM хэрэглэгчид ORDER BY xp DESC")
    if err != nil {
        return nil, err
    }
    defer rows.Close() // ← Заавал хаах!

    var жагсаалт []Хэрэглэгч
    for rows.Next() {
        var х Хэрэглэгч
        err := rows.Scan(&х.ID, &х.Нэр, &х.ИМэйл, &х.XP)
        if err != nil {
            return nil, err
        }
        жагсаалт = append(жагсаалт, х)
    }

    return жагсаалт, rows.Err()
}

// Нэг хэрэглэгч ID-аар авах
func хэрэглэгчАвах(db *sql.DB, id int) (Хэрэглэгч, error) {
    var х Хэрэглэгч
    err := db.QueryRow(
        "SELECT id, нэр, и_мэйл, xp FROM хэрэглэгчид WHERE id = ?", id,
    ).Scan(&х.ID, &х.Нэр, &х.ИМэйл, &х.XP)

    if err == sql.ErrNoRows {
        return х, fmt.Errorf("ID %d-тэй хэрэглэгч олдсонгүй", id)
    }
    return х, err
}

rows.Scan() нь SQL-ийн баганын утгыг Go-ийн хувьсагч руу хуулдаг. Баганын дараалал болон Scan-ий аргументын дараалал таарах ёстой.

Өгөгдөл шинэчлэх ба устгах

go
// XP нэмэх
func xpНэмэх(db *sql.DB, id, нэмэх int) error {
    result, err := db.Exec(
        "UPDATE хэрэглэгчид SET xp = xp + ? WHERE id = ?",
        нэмэх, id,
    )
    if err != nil {
        return err
    }

    // Хэдэн мөр өөрчлөгдсөн шалгах
    тоо, _ := result.RowsAffected()
    if тоо == 0 {
        return fmt.Errorf("ID %d-тэй хэрэглэгч олдсонгүй", id)
    }
    return nil
}

// Хэрэглэгч устгах
func хэрэглэгчУстгах(db *sql.DB, id int) error {
    _, err := db.Exec("DELETE FROM хэрэглэгчид WHERE id = ?", id)
    return err
}

Transaction — олон үйлдлийг хамтад нь

Хэрэв олон SQL команд бүгдээрээ амжилттай байх шаардлагатай бол transaction ашигладаг. Нэг нь амжилтгүй болбол бүгд буцаагдана:

go
func XPШилжүүлэх(db *sql.DB, эхID, зорьсонID, хэмжээ int) error {
    // Transaction эхлүүлэх
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    // Ямар нэг алдаа гарвал бүгдийг буцаах
    defer tx.Rollback()

    // Эхний хэрэглэгчээс XP хасах
    _, err = tx.Exec("UPDATE хэрэглэгчид SET xp = xp - ? WHERE id = ?", хэмжээ, эхID)
    if err != nil {
        return err // Rollback дуудагдана
    }

    // Хоёрдахь хэрэглэгчид XP нэмэх
    _, err = tx.Exec("UPDATE хэрэглэгчид SET xp = xp + ? WHERE id = ?", хэмжээ, зорьсонID)
    if err != nil {
        return err // Rollback дуудагдана
    }

    // Бүх зүйл амжилттай → баталгаажуулах
    return tx.Commit()
}

Transaction нь банкны шилжүүлэг, тасалбар худалдаа гэх мэт чухал үйлдлүүдэд зайлшгүй шаардлагатай.

Дараагийн хичээлд:

Database-тэй ажиллах кодоо хэрхэн найдвартай болгох вэ? Дараагийн хичээлд Go-ийн тест бичих үндсийг судална. testing package ашиглан функцуудаа автоматаар шалгах нь алдааг эрт илрүүлэх хамгийн үр дүнтэй арга юм.