tetu式

ゲームと音楽・作曲の自己満足と悩みどころの多いプログラムのブログ。

Go言語:AJAXでMySQLの結果をJSON化して受け取る

せっかくWebサーバ立てやAJAXが簡単にできるので今回はDB(MySQL)も通じて
レコードをJSON化してブラウザに返すWebアプリ王道のデータ取得をします。

今回もゴルーチンは無いよ!!


まずは使用するデータベースのドライバを入手する必要があります。
今回はMySQLです。
コンソールを開いてぱぱっと入れちゃいましょう

go get github.com/go-sql-driver/mysql

MySQLのドライバを入れたらとりあえず適当にデータベース、テーブルを作っちゃいましょう。
MySQL自体の環境を地道に作ると面倒なので自分はXamppでサクッと環境手に入れました。
完全にphpmyadminを使うためだけに入れてるのでApacheの設定も全く必要ないですし、
XamppをインストールしてApacheMySQL起動するだけで
http://localhost/phpmyadmin/ でブラウザでDB操作できちゃいます。

URLは使用しているXamppのバージョンやPCによりけりかも・・・?(Mac OS High Sierra環境でやってます)

とりあえずphpmyadminに入って適当にデータベース作ってその中にテーブルを一つ追加します
データベース名は「go_test」でテーブル名は個人的に好きなので「beatmania」にしました。
beatmaniaテーブルの中身はこんな感じ。

id(PK)	int(11)	AUTO_INCREMENT
title	varchar(256)
genre	varchar(256)
artist	varchar(256)
version	varchar(256)
bpm	varchar(256)
created	timestamp	CURRENT_TIMESTAMP	

で、データは初代beatmaniaの楽曲情報を入れます。

INSERT INTO `beatmania` (`title`, `genre`, `artist`, `version`, `bpm`) VALUES
('u gotta groove', 'HIP-HOP', 'dj nagureo', 'beatmania', '100-94-96'),
('jam jam reggae', 'REGGAE', 'Jam Master \'73', 'beatmania', '90'),
('2 gorgeous 4 u', 'BREAK-BTS', 'prophet-31', 'beatmania', '150'),
('OVERDOZER(ROMO MIX)', 'TECHNO', 'MIRAK', 'beatmania', '132'),
('Love so groovy(7inch version)', 'SOUL', 'LovemInts', 'beatmania', '141'),
('20,November(single edit)', 'HOUSE', 'n.a.r.d.', 'beatmania', '130'),
('e-motion', 'RAVE', 'e.o.s', 'beatmania', '145-140');

SP時の通常楽曲だけです。greed eaterとかも好きなんですけどね。
idはオートインクリメント、createdはデフォルトで現在時刻を入れる設定なのでINSERT文には不要。

さて、データの作成ができたのでGo言語、およびHTML側の話にうつります。
要件としては
・ページ表示時、セレクトボックスに楽曲名のリストが入る
・セレクトボックスから何かを選択し、ボタンを押したらAJAXで楽曲情報を取得し、表示する
シンプルにこれだけにします。

server.go

package main

import (
	"encoding/json"
	"fmt"
	"html/template"
	"net/http"

	"./db"
)

func getTitle(w http.ResponseWriter, r *http.Request) {
	conn := db.DbConn()
	rows, err := conn.Query("SELECT id, title from beatmania")
	if err != nil {
		panic(err.Error())
	}
	defer rows.Close()
	tableData := db.Query2Array(rows)
	jsonData, err := json.Marshal(tableData)
	if err != nil {
		panic(err.Error())
	}
	fmt.Fprintf(w, string(jsonData))
}

func getData(w http.ResponseWriter, r *http.Request) {
	conn := db.DbConn()
	rows, err := conn.Query("SELECT * from beatmania where id = ?", r.PostFormValue("title"))
	if err != nil {
		panic(err.Error())
	}
	defer rows.Close()
	tableData := db.Query2Array(rows)

	jsonData, err := json.Marshal(tableData)
	if err != nil {
		panic(err.Error())
	}
	fmt.Fprintf(w, string(jsonData))
}

func top(w http.ResponseWriter, r *http.Request) {
	tmpl := template.Must(template.ParseFiles("./html/top.html"))
	tmpl.Execute(w, nil)
}

func main() {
	_, err := db.DbInit()
	if err != nil {
		panic(err)
	}
	defer db.DbClose()

	http.Handle("/css/", http.StripPrefix("/css/", http.FileServer(http.Dir("css/"))))
	http.Handle("/script/", http.StripPrefix("/script/", http.FileServer(http.Dir("script/"))))
	http.HandleFunc("/top", top)
	http.HandleFunc("/gettitle", getTitle)
	http.HandleFunc("/getdata", getData)
	http.ListenAndServe(":8080", nil)
}

/db/db.go

package db

import (
	"database/sql"

	_ "github.com/go-sql-driver/mysql"
)

var db *sql.DB

// DB接続
func DbInit() (*sql.DB, error) {
	var err error
	db, err = sql.Open("mysql", "root:@/go_test")

	return db, err
}

// DB切断
func DbClose() {
	if db != nil {
		db.Close()
	}
}

// DBハンドラ取得
func DbConn() *sql.DB {
	return db
}

// クエリ結果(Rows)を連想配列で返す
func Query2Array(rows *sql.Rows) []map[string]interface{} {
	// Rowsからカラムを取得
	columns, err := rows.Columns()
	if err != nil {
		panic(err.Error())
	}

	// カラム数を格納
	count := len(columns)

	// 戻り値用の変数
	tableData := make([]map[string]interface{}, 0)

	// 1行レコード格納用変数と、そのポインタ用変数
	values := make([]interface{}, count)
	valuePtrs := make([]interface{}, count)

	// Rowsを回す
	for rows.Next() {
		for i := 0; i < count; i++ {
			// ポインタ格納
			valuePtrs[i] = &values[i]
		}

		// rows.Scanの引数にレコード内容が入るようになるため、ポインタ指定が必要
		// また、Rowsのカラム数と引数の項目数(配列の数でもOK)が一致する必要がある
		rows.Scan(valuePtrs...)

		// 戻り値の配列に追加する用の変数
		entry := make(map[string]interface{})
		for i, col := range columns {
			var v interface{}

			// Scan時のポインタ先に値が入っているのでvaluesで取得
			val := values[i]

			// バイト文字列だった場合はstring型にキャスト
			b, ok := val.([]byte)
			if ok {
				v = string(b)
			} else {
				v = val
			}
			entry[col] = v
		}
		tableData = append(tableData, entry)
	}
	return tableData
}

まずはGo言語部分。
一つのソースファイルで全部書こうとすると結構なスパゲティっぷりになるので、DBに関係する部分は別パッケージとしました。
server.goと同じ場所にdbフォルダを作成し、その中にdb.goファイルを入れることで、
server.goのimport内で "./db"とすることでdb.goをインポートできます。

server.goについてのポイントはあまり無いですかね・・・
ルーティングについては前回の記事でやりましたし、やってる内容も基本はDBにアクセス、クエリ発行、結果をJSON化して返すだけです。
getTitle() はidとtitleのみを取得してセレクトボックスに入れる用、 getData() で選んだタイトルの全情報を取得します。
強いてポイントをあげるなら defer ですかね。
これを入れるとそのファンクション内の最後にその処理を実行してくれるようになります。
接続してすぐに切断処理を入れられるので関連処理が並んでいい感じ。

ちなみに defer はスタック式です。
defer 処理を宣言するたび処理が積み重なって、最後に宣言した defer から処理していきます。

さて、db.goについてです。
まずimport部分、MySQLのドライバーを呼ぶところの頭に _ がついてますね。

Go言語的には関数の戻り値にエラーが含まれてたりする場合に、ブランク変数として _ を使ったりしますが・・・
importでこれを使う場合、中身は使わないけど初期化だけしておきたい、なんて時に使うみたいです。
実際のDB操作は "database/sql" の方で全てやってますし、これの内容をMySQLバージョンにする、みたいな感じですかね。

次にdbの変数宣言。var db *sql.DB です。
これはインポート先の server.go でコネクション(的なもの)を扱う為にポインタ化しています。
パッケージ化することでインポート先でそこそこ自由に使えるようになります。

DbInit() ではMySQLにアクセスする処理を入れています。
server.go の main() 最初期で呼んでますね。
sql.Open() の第2引数でDBを使用するユーザー、使用するDBを指定してます。
本来はこの辺りにユーザーログイン用のパスワードとかも必要なのでしょうが、今回はXamppインストールしたての環境なのでこれだけで大丈夫です。

DbClose() や DbConn() はシンプルにDB切断とコネクション(的なもの)を返してるだけです。

で、最後の長々とした Query2Array() です。(意味合い的にはRows2Arrayの方がよかったかな・・・?)
これは記事本文に説明を書くよりソースコードのコメントを見てやってください・・・

Go言語でDBからデータを持ってくる時、phpMySQLドライバーの操作に慣れているとどうにも使い勝手が良くなくて
わざわざこんな関数を作ることになりました。
本当はSQL文のstringを引数にしてクエリ実行から共通化したかったのですが、今回はAJAXも2種類しかないのでQuery実行後の変数を使用する形でまとめています。
で、汎用的に連想配列で返すようになっています。

DB操作の拡張ライブラリとか入れればもう少しマシになったりするかもしれないですが、
なんとなくimportは必要最小限にしたいです。

Go言語部分はこれで終了。最後にHTML側です。

/html/top.html

<html>
<head>
    <meta charset="UTF-8">
    <title>テスト</title>
	<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
	<link rel="stylesheet" type="text/css" href="../css/top.css">
	<script type="text/javascript" src="../script/top.js"></script>
</head>

<body>
	<div>
		<p class="top">bemaniなのじゃ</p>
	</div>
		タイトル:<select class="sel" name="title"></select>
		<button class="btn" name="search">データ取得</button>
	</div>
	<div class="result">
		<p class="version"></p>
		<p class="genre"></p>
		<p class="title"></p>
		<p class="artist"></p>
		<p class="bpm"></p>
	</div>
<body>

/css/top.css

.top {
	color: red;
}

/script/top.js

$(document).ready(function(){
	$sel = $('.sel')

	$.ajax({
		type: "POST",
		url: "gettitle",
		dataType: "json",
		success: function(j_data){
			$sel.empty();
			for (var i=0; i<j_data.length; i++) {
				$sel.append('<option value="' + j_data[i].id + '">' + j_data[i].title + '</option>');
			}
		}
	});
	
	$('.btn').on('click', function(){
		$.ajax({
			type: "POST",
			url: "getdata",
			data: {
				"title": $sel.val()
			},
			dataType: "json",
			success: function(j_data){
				$(".version").text("VERSION : " + j_data[0].version);
				$(".genre").text("GENRE : " + j_data[0].genre);
				$(".title").text("TITLE : " + j_data[0].title);
				$(".artist").text("ARTIST : " + j_data[0].artist);
				$(".bpm").text("BPM : " + j_data[0].bpm);
			}
		});
	});
});

内容が単純なのでHTMLに全部ぶっこんでも問題ないのですが一応フォルダ分けもしておきます。
スクリプトは期待と安心のjQuery。これ無しでJavascript書くのが嫌になる程度には仕事で使ってます。
1つ目のajaxはセレクトボックスに曲タイトルを入れるためのもので、画面読み込み時にすぐ呼ばれます。
フレームワークとか使えばHTMLファイルでヘルパー呼び出して配列を渡せばセレクトボックスを作ってくれる機能もありそうですが、
今回は使いません。

2つ目のajaxはボタンをクリックした時に反応するものです。前回の計算ボタンの流用です。
セレクトボックスが選択している曲タイトルの id をパラメータにして曲情報をセットします。


こんな感じです。
あとはserver.goをgo run コマンドで実行してhttp://localhost:8080/topにブラウザでアクセスして動きました!

作ってるうちにもっとこだわりたくなってきたのですが、あんまりやりすぎると勉強じゃなくて趣味の域になっちゃうのでこの辺で。

━━━ 追記 ━━━

server.goを go run で実行すると動くとは書きましたが、ビルドしてできた実行ファイルを実行するとサーバー自体は立つものの
以下のエラーを吐いてページが表示されませんでした。

http: panic serving [::1]:51401: open ./html/top.html: no such file or directory

実行ファイルと同じ位置にhtmlフォルダがあるのに指定したファイル、ディレクトリが見つかりませんってエラーですね。
ということは実行した場所とは違う場所で実行されているってこと・・・?

調べて見ましょう。
import 内に "path/filepath" を追加し、main() の適当な位置に fmt.Println(filepath.Abs(".")) を入れます。

server.go

import (
// 略
	"path/filepath"
)

// 略

func main() {
	fmt.Println(filepath.Abs("."))

	// 略
)

で出力した結果、
コンソールで go run server.go とした場合は実行ファイルのディレクトリが表示されました。
しかし、ビルドした実行ファイルを実行すると全く別のパスが表示されました。
どこを開いたかっていうと /Users/ユーザー名 でした。ホームフォルダですね・・・

ビルドした実行ファイルはどこで実行しようとただコンソールを開いた初期状態のフォルダ位置から実行されている、ということですかね。
実際ホームフォルダにhtml, css, scriptフォルダをコピーしたらちゃんと動きました。

まぁこれではフォルダ配置位置が限定されてしまうのでなんとかしたいといったところで以下の記事を見て無事解決できました。

golangで、実行ファイルがあるディレクトリに移動する - virsalusの日記