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)
}