Go와 MySQL에서 UPSERT操作 후 자동 증가 키 값 획득 방법

데이터베이스 설계에서 여러 테이블 간의 관계를 관리할 때, 특정 테이블의 자동 증가(AUTO_INCREMENT) 필드 값을 조회해야 하는 경우가 있다. 이 값을 다른 테이블의 외래 키로 사용해야 하기 때문이다.

다음과 같은 MySQL InnoDB 테이블을 고려해보자. 테이블에는 자동으로 증가하는 기본 키와 고유 키가 있다.

CREATE TABLE User (
    id INT UNSIGNED AUTO_INCREMENT,
    user_id INT,
    score INT,
    PRIMARY KEY (id),
    UNIQUE KEY(user_id)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

Go 프로그램을 작성하여 user_id와 score 값을 지정하고 데이터를 삽입하거나 갱신한 후, 해당 레코드의 id 값을 얻고자 한다.

단순 실행 방식

트랜잭션 없이 단일 쿼리로 처리하는 방법은 간단하지만, 자동 증가 값의 연속성을 보장하지 못한다.

package main

import (
    "database/sql"
    "log"
    _ "github.com/go-sql-driver/mysql"
)

func main() {
    db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    query := `INSERT INTO User (user_id, score) VALUES (?, ?)
              ON DUPLICATE KEY UPDATE score = VALUES(score), id = LAST_INSERT_ID(id)`

    result, err := db.Exec(query, 1001, 85)
    if err != nil {
        log.Fatal(err)
    }

    lastInsertID, err := result.LastInsertId()
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("생성된 ID: %d", lastInsertID)
}

트랜잭션 방식

자동 증가 값의 연속성이 중요한 경우, 트랜잭션을 사용하여 데이터를 처리해야 한다. 이 방식은 동시성 환경에서도 일관된 값을 보장한다.

package main

import (
    "database/sql"
    "fmt"
    "sync"

    _ "github.com/go-sql-driver/mysql"
)

func saveUserScore(db *sql.DB, uid, score int) (int, error) {
    tx, err := db.Begin()
    if err != nil {
        return 0, err
    }
    defer tx.Rollback()

    var userId int
    err = tx.QueryRow("SELECT id FROM User WHERE user_id = ?", uid).Scan(&userId)

    if err != nil {
        if err == sql.ErrNoRows {
            result, err := tx.Exec("INSERT INTO User (user_id, score) VALUES (?, ?)", uid, score)
            if err != nil {
                return 0, err
            }
            lastId, err := result.LastInsertId()
            if err != nil {
                return 0, err
            }
            userId = int(lastId)
        } else {
            return 0, err
        }
    } else {
        _, err = tx.Exec("UPDATE User SET score = ? WHERE id = ?", score, userId)
        if err != nil {
            return 0, err
        }
    }

    err = tx.Commit()
    if err != nil {
        return 0, err
    }

    return userId, nil
}

func main() {
    db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/testdb")
    if err != nil {
        panic(err)
    }
    defer db.Close()

    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            id, err := saveUserScore(db, 1000+n, 50+n)
            if err != nil {
                fmt.Printf("오류: %v\n", err)
            } else {
                fmt.Printf("사용자 ID: %d\n", id)
            }
        }(i)
    }
    wg.Wait()
}

위 구현은 자동 증가 필드의 순서를 보장한다. 실제 프로덕션 환경에서는 SELECT 쿼리를 제거하고 INSERT 또는 UPDATE만 수행하도록 최적화할 수 있다.

태그: go MySQL Database auto-increment upsert

6월 30일 17:05에 게시됨