はじめに
趣味でネイティブアプリケーションのAPIをGo言語で開発中の駆け出しエンジニアです。
普段は機械学習周りのデータサイエンスを勉強しています。ということもあり、最近初めてORMという存在を知ったので、ORMは何なのか、またGo言語のORMであるGORMの最低限の操作をまとめておこうと思います。
少しでも役に立てれば幸いです。
===2018/2/19_追記===
最近、この記事のいいねが伸びてきているのでGo言語でRedisというKVSを触った記事を書いてみました。よろしければ、そちらもどうぞ。
【Redis】Go言語で高速呼び出しKVS【Redigo】
また、pythonのORMにもまとめてみました。
【dataset】pythonでDB処理を楽チンに!【ORM】【python】
===2018/12/21_追記===
Gormを使ってAPI開発してみました。
フレームワークはechoを使っています。
【echo】Go言語でDB操作やファイル操作するミニマムAPI開発【Gorm】
ORMとは?
Object-Relational-Mappingの略で、日本語ではオブジェクト関係マッピングと呼ばれるものです。具体的には、オブジェクト指向プログラムとリレーショナルデータベースをつなぐ役割をしてくれます。
ORMを使用することでより直感的にRDBの操作をすることができると思います。
自分もORMを使う前はSQL文をベタ書きしていたのですが、ORMを使用することでコードが劇的にキレイになりました。
GORM
環境
MacOS ver10.11.6
golang ver1.7.1
インストール
go get github.com/jinzhu/gorm
操作
GORMでRDBを操作するための最低限の構文をまとめます。
RDBとの接続
以下のコードでRDBとの接続を確立します。
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
func gormConnect() *gorm.DB {
DBMS := "mysql"
USER := "root"
PASS := "####"
PROTOCOL := "tcp(##.###.##.###:3306)"
DBNAME := "##"
CONNECT := USER+":"+PASS+"@"+PROTOCOL+"/"+DBNAME
db,err := gorm.Open(DBMS, CONNECT)
if err != nil {
panic(err.Error())
}
return db
}
func main(){
db := gormConnect()
defer db.Close()
}
今回データベースはMySQLにしました。なので、gorm.Openの第一引数はmysqlを渡します。USERはmysqlに接続するアカウントを指定し、PASSにはそのパスワードを入力します。PROTOCOLには接続先のIPとポート番号を指定して、最後に接続したいDBを指定します。
以下、書き方の例です。
Ex ) gorm.Open("mysql", "root:root999@tcp(192.168.11.321:3306)/testdb")
これで実行でエラーが出なければ接続の確立完了です。
SELECT文(単一レコード)
単一レコードと複数レコードをselectで引っ張ってくるのは書き方が違うので分けて書きます。まずは、単一レコードをselectする場合です。
例で扱うテーブルを以下、eventsテーブルとします。
このテーブルを元にモデルを定義します。一応、json形式の宣言もしてあります。
type event struct {
Id int `json:id`
UserId int `json:user_id`
Summary string `json:summary`
Dtstart string `json:dtstart`
Dtend string `json:dtend`
Description string `json:description`
Year int `json:year`
Month int `json:month`
Day int `json:day`
}
ここで重要なのが、定義したモデルの名前とテーブル名が単数と複数という関係であることです。GORMではテーブルの指定がないとテーブル名を構造体の名前の複数形で参照します。今回であれば、eventsテーブルに対してeventという構造体を定義しました。さらにデータ型もテーブル情報と合わせてください。
次に単一レコードのSELECTです。
func main(){
db := gormConnect()
defer db.Close()
// 構造体のインスタンス化
eventEx := event{}
// IDの指定
eventEx.Id = 12
// 指定したIDを元にレコードを1つ引っ張ってくる
db.First(&eventEx)
// もしくはwhere句っぽく
db.First(&eventEx, "summary=?", "test")
}
基本的に構造体の情報を元にテーブルを参照して、引数に渡した構造体の情報を埋めたり、削除したりします。
この例では、eventsテーブルのidが12のレコードを引っ張ってきています。idは主キー(上図)なので、指定すれば確実にidが12のレコードを1つ取ってこれます。eventExを出力してみるとidが12のレコードの情報で埋まっているはずです。つまり、eventExが1レコードを表しています。最初に指定するのはIDに限らず、レコードを1つ特定できる情報を与えれば大丈夫です。
SELECT文(複数レコード)
次に複数レコードを引っ張ってきます。
func main(){
db := gormConnect()
defer db.Close()
// 構造体のインスタンス化
eventsEx := []event{}
// 指定した条件を元に複数のレコードを引っ張ってくる
db.Find(&eventsEx, "user_id=?", 2)
}
db.Firstの場合との違いは、引数の変数が配列である点と条件の指定の仕方です。引っ張ってきた複数のレコードを格納するために構造体を配列でインスタンス化します。そして、先ほどは直接レコード情報の何かを指定していましたが、今回はdb.Findの引数で条件を指定します。要はwhere句と同じです。なので、複数の条件で指定する時は以下のようになります。
func main(){
db := gormConnect()
defer db.Close()
// 構造体のインスタンス化
eventsEx := []event{}
// 指定した複数の条件を元に複数のレコードを引っ張ってくる
db.Find(&eventsEx, "user_id=? and year=?", 2, 2016)
}
こうすることで、user_idが2でかつyearが2016のevent情報を取ってくることができます。配列の1つ1つにeventの1レコードが格納されているはずです。
INSERT文
次に頻繁に使うであろうINSERT文です。SQL文をベタ書きすると面倒だった部分もORMを使うこと1行でかけます。
func main(){
db := gormConnect()
defer db.Close()
// 構造体のインスタンス化
eventEx := event{}
// 挿入したい情報を構造体に与える
eventEx.Id = 0
eventEx.UserId = 2
// ・
// ・
eventEx.day = 19
// INSERTを実行
db.Create(&eventEx)
}
構造体の情報をすべて埋めた上でdb.Createによってレコードを生成することができます。ここで重要な点はidが0である点です。今回、idはAUTO_INCREMENTになっているのでidを指定する必要はありません。なので、idを0にしておくと生成されたレコードは自動的にidが割り当てられて生成されます。
DELETE文
次にレコードの削除であるDELETE文です。ここは慎重にやることをオススメします。自分は試行錯誤しながらやった結果、何回かテーブル情報をすべて吹っ飛ばしました。注意すべきなのは、引数で渡すレコード情報です。削除したいレコード情報が不十分のままdeleteを行うとテーブル内のデータすべて消えます(笑)
func main(){
db := gormConnect()
defer db.Close()
// 構造体のインスタンス化
eventEx := event{}
// 削除したいレコードのIDを指定
eventEx.Id = 2
// このままdeleteを行うとテーブル内のレコードすべてが消える
// db.Delete(&eventEx)
// まずは削除したいレコードの情報を埋める
db.First(&eventEx)
// 完全にレコードを特定できる状態で削除を行う
db.Delete(&eventEx)
}
このように一度削除したいレコード情報をdb.Firstで埋めてから削除を行うことで指定した1レコードを削除できます。複数レコードを消したいときは、一度にやらずdb.Findで削除したいレコード群を引っ張てきてfor文で1レコードずつ削除すると確実だと思います。
追記(2018/7/19)
PRIMARY KEYが空だと全削除になるとコメントをいただきました。
なので以下のように直すと全削除を回避できるようです。
type event struct {
Id int `gorm:"primary_key"`
...
}
UPDATE文
最後にUPDATE文です。UPDATEするときは、更新したい古いレコードの指定と更新後の情報を持ったレコードが必要となります。
func main(){
db := gormConnect()
defer db.Close()
// 構造体のインスタンス化
eventExBefore := event{}
// 更新したいレコードを指定する
eventExBefore.Id = 2
// 更新後のレコード情報の生成(yearを2016から2015に更新)
eventExAfter := eventExBefore
db.First(&eventExAfter)
eventExAfter.year = 2015
// 更新を実行
db.Model(&eventExBefore).Update(&eventExAfter)
// 上記で更新できない場合は
db.Save(&eventExAfter)
}
少しわかりにくくなってしまいましたが、更新前と更新後のレコード情報が必要な点が重要となります。db.Modelで更新したいレコードをidで指定します。(eventExBeforeにはid情報だけ与えている)
eventExAfterは、一度Firstによって更新前の情報で埋めます。その後、更新したいところだけを更新後の情報に書き換えます。こうすることで、直感的にレコードの指定と更新がわかると思います。
以下、コードのまとめです。
package main
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
type event struct {
Id int `json:id`
UserId int `json:user_id`
Summary string `json:summary`
Dtstart string `json:dtstart`
Dtend string `json:dtend`
Description string `json:description`
Year int `json:year`
Month int `json:month`
Day int `json:day`
}
func gormConnect() *gorm.DB {
DBMS = "mysql"
USER = "root"
PASS = "####"
PROTOCOL = "tcp(##.###.##.###:3306)"
DBNAME = "##"
CONNECT = USER+":"+PASS+"@"+PROTOCOL+"/"+DBNAME
db,err := gorm.Open(DBMS, CONNECT)
if err != nil {
panic(err.Error())
}
return db
}
func main(){
db := gormConnect()
defer db.Close()
// INSERT文
// 構造体のインスタンス化
eventEx := event{}
// 挿入したい情報を構造体に与える
eventEx.Id = 0
eventEx.User_Id = 2
// ・
// ・
eventEx.day = 19
// INSERTを実行
db.Create(&eventEx)
// SELECT文(単一)
// 構造体のインスタンス化
eventEx := event{}
// IDの指定
eventEx.Id = 12
// 指定したIDを元にレコードを1つ引っ張ってくる
db.First(&eventEx)
// SELECT文(複数)
// 構造体のインスタンス化
eventsEx := []event{}
// 指定した複数の条件を元に複数のレコードを引っ張ってくる
db.Find(&eventsEx, "user_id=? and year=?", 2, 2016)
// DELETE文
// 構造体のインスタンス化
eventEx := event{}
// 削除したいレコードのIDを指定
eventEx.Id = 2
// このままdeleteを行うとテーブル内のレコードすべてが消える
// db.Delete(&eventEx)
// まずは削除したいレコードの情報を埋める
db.First(&eventEx)
// 完全にレコードを特定できる状態で削除を行う
db.Delete(&eventEx)
// UPDATE文
// 構造体のインスタンス化
eventExBefore := event{}
// 更新したいレコードを指定する
eventExBefore.Id = 2
// 更新後のレコード情報の生成(yearを2016から2015に更新)
eventExAfter := eventExBefore
db.First(&eventExAfter)
eventExAfter.year = 2015
// 更新を実行
db.Model(&eventExBefore).Update(&eventExAfter)
}
以上がORMを使った最低限のRDBの操作になります。多少まわりくどい書き方をしていますが、これでまずは動かしてみるのが良いと思います。最後にマイグレーションについてもまとめておきます。
Migration(マイグレーション)
テーブルの作成や削除をORMのマイグレーション機能で行うことができます。
type Task_Tag struct {
Id int `json:id sql:AUTO_INCREMENT`
TaskId int `json:task_id`
TagId int `json:tag_id`
}
func main(){
db := gormConnect()
defer db.Close()
db.CreateTable(&task_tag)
}
このようにテーブル情報を定義して、db.CreateTableするだけテーブルを作成することできます。sql:の部分でカラム情報をくわしく設定することできます。ちなみに下図が実際に生成されたテーブルです。