본문 바로가기
Golang

Golang database/sql 패키지 사용하기 (with Postgres) - sql.DB, Query(), QueryRow(), Exec()

by 데브겸 2023. 9. 21.

 

ORM

ORM(Object-relation mapping)은 객체지향 언어를 사용해서 SQL을 다룰 수 있게 도와주는 기술이다. ORM을 사용하게 되면 가상 객체 데이터베이스를 생성하고, 이 가상 객체 데이터베이스가 class 혹은 struct와 맵핑된다. 결과적으로 오브젝트를 만들었을 때 프레임워크가 SQL Statement를 알아서 만들어주고 그것을 실행하는 것.

(하지만 자동화되어 만들어진 Statement는 사람 손으로 만든 Statement보다 최적화된 결과물이 나오지 않을 수 있기 때문에, 복잡한 쿼리가 필요할 수록 ORM이 걸림돌이 될 수 있음)

 

 

Database/sql

Golang의 표준 패키지인 database/sql를 사용해보겠다. 

Postgres를 사용하기 위해선 pq 드라이버(https://github.com/lib/pq)가 필요하다. 드라이버를 사용하긴 하지만 명시적으로 코드로 나타내지는 않기 때문에 import가 지워질 수 있는데, 이를 방지하기 위해 앞에 _를 붙여 import해야 한다 (_ 지우면 에러 남)

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

 

sql.DB 타입

sql.DB는 database/sql에서 가장 기초가되며 중요한 타입이다. sql.DB 객체를 생성해서 쿼리도 날리고 트랜잭션도 만드는 등등 여러가지 작업을 하기 때문. sql.DB는 주로 sql.Open()을 통해 얻게 된다. 

func main() {
    db, err := sql.Open(dbDriver, dbSource) // sql.DB 객체 생성하기 (변수 db)
    defer db.Close() // db 사용 후에 닫아주기
    }

주의할 점은 sql.Open()을 했다고 해서 DB Connection이 Open 되지는 않는다는 것. 커넥션을 만드는 인자만 검증하고 직접 커넥션을 열지는 않는다(많은 경우 체크도 안 한다고 함). 실제 connection이 이뤄지는 것은 sql.DB 객체를 통해 Query등을 할 때 이뤄지게 된다.

 

Ping()으로 커넥션이 잘 되는지 확인 가능하다

func main() {
    db, err := sql.Open(dbDriver, dbSource)
    
    defer db.Close() 
    
    if err != nil {
        log.Fatal(err)
    }
    
    if err = db.Ping(); err != nil {
        log.Fatal(err)
    }
    
    }

 

 

Exec()

쿼리를 실행하되 어떠한 row도 반환하지 않는 경우 Exec() 메서드를 사용한다.

func createProductTable(db *sql.DB) {
	query := `CREATE  TABLE IF NOT EXISTS product (
		id SERIAL PRIMARY KEY,
		name VARCHAR(100) NOT NULL,
		price NUMERIC(6,2) NOT NULL,
		available BOOLEAN,
		created timestamp DEFAULT NOW()
	)`

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

}

 

 

 

 

Select - QueryRow(), Query()

조회를 할 때 하나의 row를 반환하는 경우 QueryRow() 메서드를 사용한다. 이때 조회 결과는 미리변수를 선언해두고, Scan() 메서드에 인자로 넣어 매치된 row의 cloumn 안에 있는 값을 변수 안에 넣는다.

type Product struct {
	Name      string
	Price     float64
	Available bool
}

func main() {
	product := Product{"Book", 15.55, true}
	pk := insertProduct(db, product)

	var name1 string
	var available1 bool
	var price1 float64

	query := "SELECT name, available, price FROM product WHERE id = $1"
	err = db.QueryRow(query, pk).Scan(&name1, &price1, &available1)
	if err != nil {
		if err == sql.ErrNoRows {
			log.Fatal("No rows found with ID %d", pk)
		}
		log.Fatal(err)
	}
	fmt.Printf("Name: %s\n", name1)
	fmt.Printf("Available %t\n", available1)
	fmt.Printf("Price: %f\n", price1)
}

위와 같이 name1, available1, price1 변수를 만들어두고, Scan으로 값을 넣는 방식이다

 

 

func insertProduct(db *sql.DB, product Product) int {
	query := `INSERT INTO product (name, price, available) 
		VALUES ($1, $2, $3) RETURNING id`

	var pk int
	err := db.QueryRow(query, product.Name, product.Price, product.Available).Scan(&pk)
	if err != nil {
		log.Fatal(err)
	}
	return pk
}

func main() {
    product := Product{"연필", 1000, true}
    pk := insertProduct(db, product)
}

위와 같이 Postgres의 RETURNING을 사용하는 것도 가능.

특이한 것은 db.QueryRow에서 첫번째 파라미터로 SQL query문을 받고, 뒤에 추가적으로 파라미터를 넣는 것을 볼 수 있다. 뒤에 나오는 다른 메서드들도 마찬가지이지만 이때 $1, $2, $3 과 같이 요상하게 포맷팅된 형식으로 넣을 수 있다. 이를 매개변수 표시자(placeholder parameter)라고 하며, 데이터베이스마다 그 형식이 다르다 (postgres는 $1 형식, mysql은 ? 등등) 여튼 이렇게 직접적인 문자열이 아닌 한 번 포맷팅된 것을 넣어줌으로써 sql injection 문제 등을 피할 수 있다

 

 

 

복수의 row를 반환한다면 Query()를 사용한다.  Query와 같이 복수 개의 row가 매칭된 경우 Next()와 for문을 조합하여 사용하면 된다. Rows의 계속적인 열람을 방지하기 위해 .Close()를 사용할 수 있긴 하지만, 더 이상 for문을 돌 수 없는 경우 false가 반환되고 자동으로 rows가 닫히기 때문에 써도 되고 안 써도 되긴 한다. (근데 그냥 defer rows.Close()로 쓰는게 코드가 깔끔해지는 것 같다)

func main() {

	var name2 string
	var available2 bool
	var price2 float64

	data := []Product{}
	rows, err := db.Query("SELECT name, available, price FROM product")
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close()

	for rows.Next() {
		err := rows.Scan(&name2, &available2, &price2)
		if err != nil {
			log.Fatal(err)
		}
		data = append(data, Product{name2, price2, available2})
	}

	fmt.Println(data)

}