Help us understand the problem. What is going on with this article?

【GORM】Go言語でORM触ってみた

はじめに

趣味でネイティブアプリケーションの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テーブルとします。

スクリーンショット 2016-12-30 1.41.45.png

このテーブルを元にモデルを定義します。一応、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"`
  ...
}

http://gorm.io/docs/delete.html

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によって更新前の情報で埋めます。その後、更新したいところだけを更新後の情報に書き換えます。こうすることで、直感的にレコードの指定と更新がわかると思います。

以下、コードのまとめです。

sample.go
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:の部分でカラム情報をくわしく設定することできます。ちなみに下図が実際に生成されたテーブルです。

スクリーンショット 2016-12-30 3.22.33.png

参考URL

Go言語のgormでORMしてみた
GORM Guides

chan-p
業務:データ基盤(クラウド,インフラ) AWS Solution Architect Associate 2019.6 取得 GCP Professional Cloud Architect 2020.3 取得
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした