「Goプログラミング実践入門 標準ライブラリでゼロからWebアプリを作る」という書籍を参考に、database/sqlを利用したデータベースへの接続とCRUD操作についてまとめました。
PostgreSQLへの接続
ドライバーのインストール
ドライバーをインストールする必要がある。
go get "github.com/lib/pq"
接続方法
sql.Open("postgres", "user=ユーザー名 dbname=DB名 password=パスワード sslmode=disable")
MySQLへの接続方法
ドライバーのインストール
こちらも同様にドライバーをインストールする必要がある。
go get -u github.com/go-sql-driver/mysql
接続方法
sql.Open("mysql", "ユーザー名:パスワード@tcp(ホスト:ポート番号)/DB名")
接続してみる
PostgreSQLへ接続してみます。
データベース情報
ユーザー名:gwp
DB名:gwp
パスワード:gwp
テーブル情報
create table posts (
id serial primary key,
content text,
author varchar(255)
);
コード
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
)
var Db *sql.DB
func init() {
var err error
Db, err = sql.Open("postgres", "user=gwp dbname=gwp password=gwp sslmode=disable")
if err != nil {
panic(err)
}
err = Db.Ping()
if err != nil {
fmt.Println("接続失敗")
return
} else {
fmt.Println("接続成功")
}
}
ポイント
-
_ "github.com/lib/pq"
の部分はDriverインターフェースを実装したドライバー。データベースドライバーを登録するには通常sql.Register
が使用されるがこの関数自体は書く必要がない。_ "github.com/lib/pq"
の記述をすることで、init()関数が実行された時にsql.Register
を利用しPostgreSQLをデータベースドライバーとして登録している。 - DBの種類ごとにドライバーをインポートする必要がある。
"github.com/lib/pq”
の前にある_
は参照されていないパッケージを読み込むためのもの。パッケージを利用しないがinit()関数でのみ実行が必要なので記述する。 -
*sql.DB
はデータベースへのハンドル。これを変数目Db
に入れている。Db
は変数なので他の名前でも良い。 -
Opne()
は構造体sql.DB
へのポインタを返す。(データベース接続に必要になる構造体) - sql.Open()で返される構造体はこんな感じ
&{0 {user=gwp dbname=gwp password=gwp sslmode=disable 0x144c750} 0 {0 0} [] map[] 0 0 0xc00007e240 false map[] map[] 0 0 0 0 <nil> 0 0 0 0 0x107ffa0}
- 実際にデータベースへの接続を確認しているのは
err = Db.Ping()
の部分。ここがエラーだと接続できていない、ということ。
投稿の作成
func (post *Post) Create() (err error) {
statement := "insert into posts (content, author) values ($1, $2) returning id"
stmt, err := Db.Prepare(statement)
if err != nil {
return
}
defer stmt.Close()
err = stmt.QueryRow(post.Content, post.Author).Scan(&post.Id)
return
}
func main() {
post := Post{Content: "Hello World!", Author: "Amby"}
post.Create()
}
プリペアドステートメント
動的にSQLを生成する必要がある時、変更箇所だけを変数のようにしたSQL文を作成しておく方式のこと。
SQLインジェクションを防止する効果がある。
ポイント
-
Create()
内ではプリペアドステートメントを利用している。&1
,&2
はレコードを作成する時に実際の値で置き換えられる。 -
stmt, err := Db.Prepare(statement)
の部分でステートメントを生成している。Prepare()
はインターフェースsql.Stmt
への参照を生成する。stmt
の中身はこんな感じになっている。これが使用するステートメント。
&{0xc00010a8f0 insert into posts (content, author) values ($1, $2) returning id <nil> {{0 0} 0 0 0 0} <nil> 0xc0001a6000 <nil> {0 0} false [{0xc0001a0000 0xc0001a6000}] 0}
-
err = stmt.QueryRow(post.Content, post.Author).Scan(&post.Id)
の部分でプリペアドステートメントを実行している。QueryRow()
は1レコードを取得する。先に生成されたプリペアドステートメントstmt
に対してQueryRow()
を実行している点に注意。 -
Scan
はfmtパッケージの関数で、Scan(&格納先の変数)
とすることで、入力を「&格納先の変数」に格納することができる。この場合、stmt.QueryRow(post.Content, post.Author)
で返ってくる値を&post.Id
に格納している。(プリペアドステートメントでreturning id
としているのでidが返ってきている。)
投稿の取得
func GetPost(id int) (post Post, err error) {
post = Post{}
err = Db.QueryRow("select id, content, author from posts where id = $1", id).Scan(&post.Id, &post.Content, &post.Author)
return
}
func main() {
readPost, _ := GetPost(post.Id)
}
ポイント
- 構造体Postを返したいので、まず空の構造体Postを作成している。
- 投稿作成時と同様で、Scanを使うことで
(&post.Id, &post.Content, &post.Author)
に返ってきた値を格納している。 - 今回のケースではプリペアドステートメントは使用していない(使用しても良い)。プリペアドステートメントを使用しない場合、
sql.DB
のメソッドであるQueryRow
を使い、SQLを直接実行している。
投稿の更新
func (post *Post) Update() (err error) {
_, err = Db.Exec("update posts set content = $2, author = $3 where id = $1", post.Id, post.Content, post.Author)
return
}
func main() {
readPost.Content = "Wahahahahaha"
readPost.Author = "Goromaru"
readPost.Update()
}
ポイント
-
Update()
は構造体Postに対するメソッドとして定義する。 -
sql.DB
のメソッドであるExec()
を使用している。プリペアドステートメントを使用するよりも高速。 - エラーがあればエラーが返されるが、そうでなければ
_
によって返り値は無視される。
投稿の削除
func (post *Post) Delete() (err error) {
_, err = Db.Exec("delete from posts where id = $1", post.Id)
return
}
func main() {
readPost.Delete()
}
ポイント
- 投稿の更新とほぼ同じ
全投稿の取得
func Posts(limit int) (posts []Post, err error) {
rows, err := Db.Query("select id, content, author from posts limit $1", limit)
if err != nil {
return
}
for rows.Next() {
post := Post{}
err = rows.Scan(&post.Id, &post.Content, &post.Author)
if err != nil {
return
}
posts = append(posts, post)
}
rows.Close()
return
}
func main() {
posts, _ = Posts(10)
}
ポイント
- データベースから最初の10件のPostを取得する
-
Db.Query
の部分はsql.DB
のメソッドでQuery()
を使用している。このメソッドはインターフェースRowsを返す。 - Rowsはイテレータ。
-
posts, _ = Posts(10)
の返り値はこのようになる
[{1 Wahahahahaha Goromaru}...]
1対多の関係
テーブルの用意
create table posts (
id serial primary key,
content text,
author varchar(255)
);
create table comments (
id serial primary key,
content text,
author varchar(255),
post_id integer references posts(id)
);
ポイント
- commentsテーブルにpost_idカラムを用意している。これは
references posts(id)
とすることでpostsテーブルのid列を参照することになり、postsのidを参照する外部キーになる。
関係構築
type Post struct {
Id int
Content string
Author string
Comments []Comment
}
type Comment struct {
Id int
Content string
Author string
Post *Post
}
ポイント
- 構造体PostにCommentsというフィールドを用意している。これは構造体Commentのスライスになっている。つまり、Postに関係するCommentをこのフィールドの中に格納する。
- 構造体CommentにはPostというフィールドがある。これは構造体Postへのポインタになっている。つまりCommentがどのPostに関係するか?を格納するフィールドになっている。
- 構造体PostのCommentsフィールド、構造体CommentのPostフィールドは共にポインタである。なぜなら関係する構造体のコピーを保持するのではなく、参照をしたいから。
Commentの作成
func (comment *Comment) Create() (err error) {
if comment.Post == nil {
err = errors.New("投稿が見つかりません")
return
}
err = Db.QueryRow("insert into comments (content, author, post_id) values ($1, $2, $3) returning id", comment.Content, comment.Author, comment.Post.Id).Scan(&comment.Id)
return
}
func main() {
post := Post{Content: "Hello World!", Author: "Sau Sheong"}
post.Create()
comment := Comment{Content: "いい投稿だね!", Author: "Joe", Post: &post}
comment.Create()
}
ポイント
-
comment := Comment{Content: "いい投稿だね!", Author: "Joe", Post: &post}
の部分で構造体Commentを作成する際にPost: &post
としてリレーションを作成している。この&post
はpost := Post{Content: "Hello World!", Author: "Sau Sheong"}
へのポインタ。 -
Create()
は先に見た投稿の作成とおなじ(プリペアドステートメントを使っていない点は異なる)。SQL文でreturning id
としているのでidが返ってきて、それを.Scan(&comment.Id)
でcommentのidに設定している。
関係の取得
func GetPost(id int) (post Post, err error) {
post = Post{}
post.Comments = []Comment{}
err = Db.QueryRow("select id, content, author from posts where id = $1", id).Scan(&post.Id, &post.Content, &post.Author)
rows, err := Db.Query("select id, content, author from comments where post_id = $1", id)
if err != nil {
return
}
for rows.Next() {
comment := Comment{Post: &post}
err = rows.Scan(&comment.Id, &comment.Content, &comment.Author)
if err != nil {
return
}
post.Comments = append(post.Comments, comment)
}
rows.Close()
return
}
func main() {
readPost, _ := GetPost(post.Id)
}
ポイント
- まず
post = Post{}
を作成する。これはGetPost()
の返り値。 -
post.Comments = []Comment{}
とし、取得したCommentをPost{}
に格納できるようにする。 -
rows
はインターフェースRowであり、イテレータ。Commentのレコードが複数件格納されている。 -
comment := Comment{Post: &post}
の部分でpost.Comments
のスライスの中に格納するCommentを生成している。このCommentは全てpost
(引数で受け取ったidに対応するPost)のポインタに紐づいている。 -
append(post.Comments, comment)
はスライスであるpost.Comments
にcomment
を追加している。
まとめ
非常にシンプルにデータベース接続とCRUD操作が実装できると思いました。