ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Golang] Database/sql
    언어/Golang 2016. 11. 26. 16:27

    Package

    SQL 데이타베이스를 사용하기 위해서는 표준패키지 database/sql을 사용합니다. database/sql 패키지는 관계형 데이타베이스들에게 공통적으로 사용되는 인터페이스들을 제공하고 있습니다.

    database/sql 패키지는 여러 종류의 SQL 데이타베이스를 지원하는데, 각각의 데이타베이스 Driver와 함께 사용됩니다.


    아래는 Postgresql을 연결하기 위해 사용한 예제입니다.

    import (
    	_ "github.com/lib/pq"
    	"database/sql"
    )

    만약 드라이버 ("github.com/lib/pq") 부분을 사용하지 않는다는 표시로 앞에 _를 붙였는데 쓰지 않는다고 지우면 에러납니다. ( 연결을 위해 명시적으로 드라이버를 지정해야 함)

    Connect

    database/sql 패키지에서 가장 중요한 Type은 sql.DB 인데, 일반적으로 sql.Open() 함수를 사용하여 sql.DB 객체를 얻습니다. 즉, sql.Open(드라이버, Connection) 함수에서 어떤 DB 드라이버를 사용할 것인지, 그리고 해당 DB의 연결 정보를 제공하면, 결과로 sql.DB 객체를 얻게 됩니다. 일단 이 sql.DB 객체를 얻은 후, sql.DB의 여러 메서드들을 사용하여 쿼리를 하고, SQL문을 실행합니다.

    주목할 점은 sql.Open()은 실제 DB Connection을 Open하지 않는다는 점입니다. 즉, sql.DB는 드라이버종류와 Connection 정보를 가지고는 있지만, 실제 DB를 연결하지 않으며, 많은 경우 Connection 정보 조차 체크하지도 않습니다. 실제 DB Connection은 Query 등과 같이 실제 DB 연결이 필요한 시점에 이루어지게 됩니다.


    아래는 Postgresql에 연결하는 예시입니다. 

    // 예시
    db, err := sql.Open(드라이버명, `host= db의 ip port= db포트 user=접속할 db 유저 password=접속할 db pwd dbname=접속할 db명 sslmode=disable`)
     
    // sql.DB 객체 생성
    db, err := sql.Open("postgres", `host= localhost port=5432
    	user=postgres password=postgres dbname=postgres sslmode=disable`)
     
    // db가 전부 사용한 다음 맨 마지막에 닫도록 해주는 명령
    defer db.Close()
     
    //db.ping() 만약 db연결이 실패할 경우 왜 실패한건지 에러찍는 용도
    if err != nil || db.Ping() != nil {
    		panic(err.Error())
    }

    이후 설명은 위의 정보가 유지된다는 가정하에 진행하겠습니다.

    Select

    Go에서는 조회를 할 때 2개의 함수를 제공합니다. 하나의 Row만 리턴할 경우( 또는 1개의 Row만 리턴이 될 것을 예상한 경우) QueryRow() 메서드, 복수개의 Row를 리턴한다면 Query() 메서드를 사용합니다. 하나의 Row에서 실제 데이타를 읽어 로컬 변수에 할당하기 위해 Scan() 메서드를 사용하며, 복수 Row에서 다음 Row로 이동하기 위해 Next() 메서드를 사용합니다.

    QueryRow()

    하나의 Row를 출력하는 예제입니다.

    var name string
    err := db.QueryRow("SELECT name FROM test WHERE id = 10").Scan(&name)
    if err != nil {
            panic(err.Error())
        }
    fmt.Println(name)

    Query()

    다수의 Row에서 다음 Row로 이동하기위해 Next() 메서드를 사용하는데, 반복문을 사용하여 체크합니다.

    	var id int
        var name string
        rows, err := db.Query("SELECT id, name FROM test1 where id >= $1", 1)
        if err != nil {
            log.Fatal(err)
        }
        defer rows.Close() //반드시 닫는다 (지연하여 닫기)
     
        for rows.Next() {
            err := rows.Scan(&id, &name)
            if err != nil {
                log.Fatal(err)
            }
            fmt.Println(id, name)
        }

    여기서 한가지 주목할 것은 SQL 쿼리에서 $1 (Placeholder)를 사용하여 Parameterized Query를 사용하고 있다는 점입니다. 이는 SQL Injection과 같은 문제를 방지하기 위해 파라미터를 문자열 결합이 아닌 별도의 파라미터로 대입시키는 방식입니다. 위의 예제에서 Placeholder $1 에는 1이 대입됩니다. Placeholder는 데이타베이스의 종류에 따라 다르게 사용하는데, 예를 들어 MySql은 ? 를 사용하고, Oracle은 :val1, :val2 등을 사용하고, PostgreSQL은 $1, $2 등을 사용합니다.


    경험담으로 아래와 같이 데이터베이스의 스키마를 변경한 후에 Query() 메서드를 호출하고 난 다음 함수를 빠져나가면 스키마가 다시 public으로 변경됩니다. (QueryRow()는 그대로 유지)

    func setSearchPath() {
    	//스키마 변경 
    	_, err := db.Exec("SET SEARCH_PATH TO abcd")
    	if err != nil {
    panic(err)
    }
    }

    func queryRow() {
    	var name string
    	err := db.QueryRow("SELECT name FROM test WHERE id = 10").Scan(&name)
    	if err != nil {
            panic(err.Error())
        }
    }

    func query() {
    	var id int
        var name string
        rows, err := db.Query("SELECT id, name FROM test1 where id >= $1", 1)
        if err != nil {
            log.Fatal(err)
        }
        defer rows.Close() //반드시 닫는다 (지연하여 닫기)
     
        for rows.Next() {
            err := rows.Scan(&id, &name)
            if err != nil {
                log.Fatal(err)
            }
            fmt.Println(id, name)
        }
    }

    func main() {
    setSearchPath()
    	query() // 함수호출한 다음, 변경된 스키마가 다시 public으로 돌아옴
    queryRow() // 함수호출한 다음 변경된 스키마 유지
    }

    Insert, Delete, Update

    데이터를 INSERT, UPDATE, DELETE (DML Operation)하기 위해서 sql.DB 객체의 Exec() 메서드를 사용합니다. Query/QueryRow 메서드는 데이타를 리턴할 때 사용하는 반면, DML과 같이 리턴되는 데이터가 없는 경우는 Exec 메서드를 사용해야 합니다.


    	// INSERT 문 실행
    result, err := db.Exec("INSERT INTO test1 VALUES ($1, $2)", 11, "Jack")
    if err != nil {
    log.Fatal(err)
    }

    // sql.Result.RowsAffected() 체크
    n, err := result.RowsAffected()
    if n == 1 {
    fmt.Println("1 row inserted.")
    }

    Exec 메서드의 첫번째 파라미터에는 SQL문을 적고, 그 SQL문 안에 $1, $2 이 있는 경우 계속해서 상응하는 파라미터를 넣어 줍니다. Exec 메서드는 sql.Result와 error 객체를 리턴하며, sql.Result 객체로부터 갱신된 레코드수(RowsAffected())와 새로 추가된 Id (LastInsertId())를 구할 수 있습니다.

    Prepared Statement

    Prepared Statement는 데이터베이스 서버에 Placeholder를 가진 SQL문을 미리 준비시키는 것으로, 차후 해당 Statement를 호출할 때 준비된 SQL문을 빠르게 실행하도록 하는 기법입니다. Go에서 Prepared Statement를 사용하기 위해서는 sql.DB의 Prepare() 메서드를 써서 Placeholder를 가진 SQL문을 미리 준비시키고, sql.Stmt 객체를 리턴받습니다. 차후 이 sql.Stmt 객체의 Exec (혹은 Query/QueryRow) 메서드를 사용하여 준비된 SQL문을 실행합니다.


    	// Prepared Statement 생성
    stmt, err := db.Prepare("UPDATE test1 SET name=? WHERE id=?")
    checkError(err)
    defer stmt.Close()

    // Prepared Statement 실행
    _, err = stmt.Exec("Tom", 1) //Placeholder 파라미터 순서대로 전달
    checkError(err)
    _, err = stmt.Exec("Jack", 2)
    checkError(err)
    _, err = stmt.Exec("Shawn", 3)
    checkError(err)
    }

    func checkError(err error) {
    if err != nil {
    panic(err)
    }

    트랜잭션

    복수 개의 SQL 문을 하나의 트랜잭션으로 묶기 위하여 sql.DB의 Begin() 메서드를 사용합니다. 트랜잭션은 복수 개의 SQL 문을 실행하다 중간에 어떤 한 SQL문에서라도 에러가 발생하면 전체 SQL문을 취소하게 되고 (롤백), 모두 성공적으로 실행되어야 전체를 커밋하게 됩니다. sql.Tx 타입의 Begin() 메서드는 sql.Tx 객체를 리턴하는데, 마지막에 최종 Commit을 위해 Tx.Commit() 메서드를, 롤백을 위해 Tx.Rollback() 메서드를 호출합니다.


    	// 트랜잭션 시작
    tx, err := db.Begin()
    if err != nil {
    log.Fatal(err)
    }
    defer tx.Rollback() //중간에 에러시 롤백

    // INSERT 문 실행
    _, err = db.Exec("INSERT INTO test1 VALUES (?, ?)", 15, "Jack")
    if err != nil {
    log.Fatal(err)
    }

    _, err = db.Exec("INSERT INTO test2 VALUES (?, ?)", 15, "Data")
    if err != nil {
    log.Fatal(err)
    }

    // 트랜잭션 커밋
    err = tx.Commit()
    if err != nil {
    log.Fatal(err)
    }



    레퍼런스: https://golang.org/pkg/database/sql/

    댓글