0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Goのdatabase/sqlを利用してPostgreSQL/MySQLへ接続&CRUD操作してみた

Posted at

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としてリレーションを作成している。この&postpost := 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.Commentscommentを追加している。

まとめ

非常にシンプルにデータベース接続とCRUD操作が実装できると思いました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?