Golang database/sql 패키지 삽질기 - 2편 SQLite 메모리 데이터베이스

SQLite 데이터베이스는 파일뿐만 아니라 메모리 모드도 지원한다. 그래서 필자는 데이터베이스 테스트 픽스처로서 주로 SQLite 메모리 데이터베이스를 사용한다. 데이터베이스 드라이버는 프로그래밍 언어에서 데이터베이스를 다룰 때 필요하다. 필자의 경우 go-sqlite3 드라이버 사용했다. 문제는 데이터베이스 접속 URL에 따라 상이하게 동작한다는 것이다.

https://github.com/mattn/go-sqlite3

https://github.com/mattn/go-sqlite3

이 글은 엄밀히 말하면 go-sqlite3 드라이버를 사용하면서 겪었던 삽질이다. 다만 database/sql 패키지를 함께 쓰면서 겪었던 일이라서 엮어서 소개한다.


  1. 매개변수 표시자
  2. SQLite 메모리 데이터베이스
  3. 커넥션 풀


No such table?

아래 코드는 SQLite를 메모리 데이터베이스로 사용하며 parent, child 테이블을 만들고 테스트 데이터를 넣는다. 그리고 parent 테이블과 child 테이블의 데이터를 출력한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package main
import (
    "database/sql"
    "fmt"
    _ "github.com/mattn/go-sqlite3"
    "log"
)
func main() {
    // 메모리 데이터베이스
    db, err := sql.Open("sqlite3", ":memory:")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
    db.Exec("CREATE TABLE `parent` (`id` int(11) NOT NULL, `name` varchar(100) NOT NULL, PRIMARY KEY (`id`))")
    db.Exec("INSERT INTO `parent` (`id`,`name`) VALUES (1,'부모A')")
    db.Exec("INSERT INTO `parent` (`id`,`name`) VALUES (2,'부모B')")
    db.Exec("CREATE TABLE `child` (`id` int(11) NOT NULL, `name` varchar(45) DEFAULT NULL, `parent_id` int(11) NOT NULL,PRIMARY KEY (`id`))")
    db.Exec("INSERT INTO `child` (`id`,`name`,`parent_id`) VALUES (1,'자식C',1)")
    db.Exec("INSERT INTO `child` (`id`,`name`,`parent_id`) VALUES (2,'자식D',1)")
    db.Exec("INSERT INTO `child` (`id`,`name`,`parent_id`) VALUES (3,'자식E',2)")
    parentRows, err := db.Query("SELECT id, name FROM parent")
    if err != nil {
        log.Fatal(err)
    }
    var parentId int64
    var parentName string
    for parentRows.Next() {
        err := parentRows.Scan(&parentId, &parentName)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Println(parentId, ":" ,parentName)
        var childId int64
        var childName string
        childRows, err := db.Query("SELECT id, name FROM child WHERE parent_id = $1", parentId)
        if err != nil {
            log.Fatal(err)
        }
        for childRows.Next() {
            err := childRows.Scan(&childId, &childName)
            if err != nil {
                log.Fatal(err)
            }
            fmt.Println("ㄴ", childId, ":" ,childName)
        }
        childRows.Close()
    }
    parentRows.Close()
}

코드를 실행해 보면 no such table: child 오류를 만난다.

image2019-5-20_9-16-51

오류 지점은 바로 아래 코드이다.

1
childRows, err := db.Query("SELECT id, name FROM child WHERE parent_id = $1", parentId)

무슨 말일까? parent 테이블은 있고, child 테이블은 없다니...

같은 코드를 메모리 데이터베이스가 아닌 파일 데이터베이스로 변경해서 실행해보자.

1
2
3
4
5
6
7
package main
//...
func main() {
    // 파일 데이터베이스
    db, err := sql.Open("sqlite3", "file:article.db")
    //...
}

오류 없이 동작한다.

image2019-5-24_8-5-29

왜 메모리 데이터베이스에만 오류가 나는 것일까? 답은 역시 go-sqlite3 드라이버 문서에서 찾을 수 있었다. 그것도 FAQ에서..(많이 질문한다는 의미)

문서에 따르면...

필자는 앞서 예시 코드에서 메모리 데이터베이스를 사용하기 위해  ":memory:" 로 선언했다.

1
2
// 메모리 데이터베이스
db, err := sql.Open("sqlite3", ":memory:")

이 경우 데이터베이스 커넥션들은 동시에 하나의 메모리 데이터베이스를 공유하지 않는다. 즉 데이터베이스 커넥션이 열려 있는 상태에서 새로운 커넥션을 열게 되면 기존 데이터베이스가 아닌 새로운 빈 데이터베이스를 할당받는다는 의미이다.

Why I'm getting no such table error? Why is it racy if I use asql.Open("sqlite3", ":memory:")database? Each connection to :memory: opens a brand new in-memory sql database, so if the stdlib's sql engine happens to open another connection and you've only specified ":memory:", that connection will see a brand new database. A workaround is to use "file::memory:?mode=memory&cache=shared". Every connection to this string will point to the same in-memory database. - https://github.com/mattn/go-sqlite3#faq

코드를 보며 좀 더 구체적으로 알아보자. 예시 코드에서는 커넥션 1을 생성하고 종료하지 않는 상태에서 커넥션 2를 생성한다. 이미 커넥션 1에서 메모리 데이터베이스를 사용하고 있기 때문에 커넥션 2는 새로운 빈 데이터베이스를 할당받는다. 따라서 테이블 스키마가 존재하지 않기 때문에 no such table 오류가 난 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 커넥션 1 생성
parentRows, err := db.Query("SELECT id, name FROM parent")
if err != nil {
    log.Fatal(err)
}
// ...
for parentRows.Next() {
    // ...
    // 커넥션 2 생성
    childRows, err := db.Query("SELECT id, name FROM child WHERE parent_id = $1", parentId)
    if err != nil {
        log.Fatal(err)
    }
    // ...
    // 커넥션 2 종료
    childRows.Close()
}
// 커넥션 1 종료
parentRows.Close()

결론

go-sqlite3 드라이버 문서 권고 따라 아래처럼 변경하면 모든 커넥션이 같은 데이터베이스를 공유하기 때문에 오류 없이 동작한다.

1
2
// 메모리 데이터베이스
db, err := sql.Open("sqlite3", "file::memory:?mode=memory&cache=shared")

Popit은 페이스북 댓글만 사용하고 있습니다. 페이스북 로그인 후 글을 보시면 댓글이 나타납니다.