📝

【完全解説】GolangとGORMで作るシンプルなメモアプリ

に公開

1. はじめに

これまで、GolangとSQLiteを使ってシンプルなコマンドラインメモアプリを作成する方法を学びました。直接SQL文を用いてDB操作を行う実装は、動作確認もしやすく実践的でしたが、コード量が増えたり、保守性の面で課題が出たりすることもありました。

本記事では、そのアプローチから一歩進んで、ORM (Object Relational Mapping) を利用して、より簡潔で読みやすく、拡張しやすいコードの書き方を解説します。ORMを使うことで、SQL文を書く手間が大幅に軽減され、データベース操作がオブジェクト指向の感覚で行えるようになります。この記事では、Golangで人気のあるORMライブラリであるGORMを使い、前回のメモアプリをORM版にリファクタリングする過程を、ステップバイステップで学んでいきます。

前回のメモアプリのコード
package main

import (
	"database/sql"
	"fmt"
	"log"
	"os"
	"strconv"

	_ "github.com/mattn/go-sqlite3"
)

func main() {
	// コマンドライン引数がなければ終了
	if len(os.Args) < 2 {
		os.Exit(1)
	}

	// SQLite DB を memo.db としてオープン (存在しなければ作成)
	db, err := sql.Open("sqlite3", "memo.db")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// memos テーブルがなければ作成する
	createTableSQL := `
	CREATE TABLE IF NOT EXISTS memos (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		content TEXT NOT NULL);`
	if _, err = db.Exec(createTableSQL); err != nil {
		log.Fatal(err)
	}

	// コマンドライン引数の解析と処理の分岐
	cmd := os.Args[1]
	switch cmd {
	case "add":
		// メモ追加コマンド: 引数が足りなければ使い方を表示して終了
		if len(os.Args) < 3 {
			fmt.Println("Please provide a memo to add")
			usage()
			os.Exit(1)
		}
		memoText := os.Args[2]
		addMemo(db, memoText)
	case "list":
		// メモ一覧表示コマンド: そのまま listMemos 関数を呼び出す
		listMemos(db)
	case "delete":
		// メモ削除コマンド: 削除するメモのIDが必要
		if len(os.Args) < 3 {
			fmt.Println("Please provide a memo ID to delete")
			usage()
			os.Exit(1)
		}
		idStr := os.Args[2]
		id, err := strconv.Atoi(idStr)
		if err != nil {
			fmt.Println("Invalid ID:", idStr)
			os.Exit(1)
		}
		deleteMemo(db, id)
	default:
		// 道のコマンドの場合は使い方を表示
		fmt.Println("Invalid command:", cmd)
		usage()
		os.Exit(1)
	}
}

func usage() {
	fmt.Println(`Usage:
	memo add <memo>
	memo list
	memo delete <memo ID>`)
}

func addMemo(db *sql.DB, content string) {
	stmt, err := db.Prepare("INSERT INTO memos (content) VALUES (?)")
	if err != nil {
		log.Fatal(err)
	}
	defer stmt.Close()

	_, err = stmt.Exec(content)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("Memo added successfully.")
}

func listMemos(db *sql.DB) {
	rows, err := db.Query("SELECT id, content FROM memos")
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close()

	fmt.Println("Memo list:")
	for rows.Next() {
		var id int
		var content string
		err = rows.Scan(&id, &content)
		if err != nil {
			log.Fatal(err)
		}
		fmt.Printf("%d: %s\n", id, content)
	}

	// ループ後にエラーがないか確認する
	if err = rows.Err(); err != nil {
		log.Fatal(err)
	}
}

func deleteMemo(db *sql.DB, id int) {
	stmt, err := db.Prepare("DELETE FROM memos WHERE id = ?")
	if err != nil {
		log.Fatal(err)
	}
	defer stmt.Close()

	res, err := stmt.Exec(id)
	if err != nil {
		log.Fatal(err)
	}

	count, err := res.RowsAffected()
	if err != nil {
		log.Fatal(err)
	}

	if count == 0 {
		fmt.Println("No memo found with given ID.")
	}else{
		fmt.Println("Memo deleted successfully.")
	}
}

2. ORMとは何か?

ORM (Object Relational Mapping) は、プログラミング言語のオブジェクトと、リレーショナルデータベースのテーブルとの間でデータのマッピングを自動的に行う仕組みです。ORMを使用することで、SQL文を直接記述する代わりに、オブジェクト指向のインターフェースを通じてデータベース操作を行うことが可能となります。

具体的には、以下のようなメリットがあります:

  • コードの簡潔化:
    オブジェクトとしてデータベースのレコードを扱えるため、SQL文の記述量が大幅に減ります。

  • 保守性の向上:
    データベーススキーマが変更になった場合でも、モデル(構造体)の定義を修正するだけで済むため、アプリケーション全体の保守が容易になります。

  • 抽象化:
    複雑なSQLクエリの構築や、異なるデータベース間での移行が、ORMによる抽象化のおかげでスムーズに行えます。

Golangでは、ORMライブラリとしてGORMが広く利用されており、今回の記事ではこのGORMを利用して、先ほどのメモアプリをより洗練された形にアップデートしていきます。これにより、開発効率が向上するだけでなく、コードの読みやすさや保守性も高まる点を実感できるでしょう。

3. Golangで使えるORMライブラリの紹介

Golangには、リレーショナルデータベースとの連携を容易にするためのORMライブラリがいくつか存在します。代表的なものをいくつかご紹介します。

  • GORM
    Golangで最も人気のあるORMライブラリです。直感的なAPIと柔軟なモデル定義、オートマイグレーション機能などが特徴で、SQLiteをはじめとする多くのデータベースに対応しています。

  • Ent
    Facebookが開発したエンタープライズ向けのORMです。コード生成を利用して強い型付けのモデルを生成するため、コンパイル時に多くのエラーを検出できるのが魅力です。

  • XORM
    シンプルで軽量なORMライブラリです。使い方が直感的で、データベース操作における柔軟性と高速性が特徴です。

今回の記事では、使いやすさやドキュメントの充実度から、特にGORMに焦点を当てて解説していきます。

4. GORMのインストールと基本設定

ここでは、GORMをインストールし、SQLiteと連携するための基本設定について説明します。

GORMのインストール

GORM自体とSQLiteドライバをインストールするには、以下のコマンドを実行します。

go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite

基本設定

GORMを利用してSQLiteデータベースに接続する基本的なコードは以下の通りです。ここでは、先ほどのメモアプリ用に memo.db を使用します。

package main

import (
	"fmt"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func main() {
	// SQLite データベースに接続。ファイルが存在しない場合は自動で作成される
	db, err := gorm.Open(sqlite.Open("memo.db"), &gorm.Config{})
	if err != nil {
		fmt.Println("failed to connect database:", err)
		return
	}
}

5. モデル定義とマイグレーション

GORMでは、テーブルに対応する構造体(モデル)を定義することで、オブジェクト指向的にDB操作が可能です。例えば、メモアプリの memos テーブルに対応するモデルは以下のように定義できます。

type Memo struct {
	ID      uint   `gorm:"primaryKey"`
	Content string `gorm:"not null"`
}

そして、以下のコードで自動マイグレーションを実行することで、モデルに基づいたテーブルが作成されます。

db.AutoMigrate(&Memo{})

このように、GORMを利用すると、モデル定義と自動マイグレーションにより、テーブル作成の手間が大幅に削減され、コードもシンプルに保てます。

プログラムの全体像
package main

import (
	"fmt"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func main() {
	// SQLite データベースに接続。ファイルが存在しない場合は自動で作成される
	db, err := gorm.Open(sqlite.Open("memo.db"), &gorm.Config{})
	if err != nil {
		fmt.Println("failed to connect database:", err)
		return
	}

	// 自動マイグレーションを実行することで、モデルに基づいたテーブルが作成される
	db.AutoMigrate(&Memo{})
}

type Memo struct {
	ID      uint   `gorm:"primaryKey"`
	Content string `gorm:"not null"`
}

6. CRUD操作の実装

ここでは、GORMを使ってメモアプリの基本的なCRUD操作(Create, Read, Delete)を実装する方法を解説します。今回のアプリでは、以下の操作を行います。

  • Create (追加): ユーザーが入力したメモをDBに保存する
  • Read (一覧表示): DBに保存されたメモを取得して一覧表示する
  • Delete (削除): 指定されたIDのメモをDBから削除する

GORMでは、各操作に対してシンプルなメソッドが用意されているため、コードが非常に簡潔になります。以下に、それぞれの操作の実装例を示します。

Create(追加)

GORMの Create メソッドを使うことで、構造体に対応するレコードをDBに挿入できます。たとえば、Memo モデルのインスタンスを作成し、それをDBに保存します。

func addMemo(db *gorm.DB, content string) {
	memo := Memo{Content: content}
	result := db.Create(&memo)
	if result.Error != nil {
		log.Fatal(result.Error)
	}
	fmt.Printf("Memo added successfully. ID: %d\n", memo.ID)
}

このコードでは、 Memo 構造体のインスタンスにメモ内容を設定し、 db.Create によりDBに挿入しています。挿入が成功すると、自動的に生成されたIDも memo.ID に格納されます。

Read(一覧表示)

GORMの Find メソッドを使って、DBから全てのメモレコードを取得し、一覧表示する実装例です。

func listMemos(db *gorm.DB){
	var memos []Memo
	result := db.Find(&memos)
	if result.Error != nil {
		log.Fatal(result.Error)
	}
	fmt.Println("Memo list:")
	for _, m := range memos {
		fmt.Printf("%d: %s\n", m.ID, m.Content)
	}
}

このコードでは、 memos スライスに全てのレコードを取得し、各レコードのIDと内容を表示しています。

Delete(削除)

GORMの Delete メソッドを利用して、指定したIDのレコードを削除します。削除後、影響を受けた行数を確認して、該当するメモが存在したかどうかを判断します。

func deleteMemo(db *gorm.DB, id uint) {
	result := db.Delete(&Memo{}, id)
	if result.Error != nil {
		log.Fatal(result.Error)
	}
	if result.RowsAffected == 0 {
		fmt.Println("No memo found with given ID.")
	}else{
		fmt.Println("Memo deleted successfully.")
	}
}

このコードでは、 db.Delete を呼び出す際に、削除対象のモデル(ここでは Memo{})とIDを指定します。削除に成功すれば「Memo deleted successfully.」と表示され、該当するレコードがなければ「No memo found with given ID.」と出力されます。

プログラムの全体像

以下は、これまでのCRUD操作を含む、GORMを使った最終的なメモアプリの全体コードの例です。

package main

import (
	"fmt"
	"log"
	"os"
	"strconv"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func main() {
	// SQLite データベースに接続。ファイルが存在しない場合は自動で作成される
	db, err := gorm.Open(sqlite.Open("memo.db"), &gorm.Config{})
	if err != nil {
		fmt.Println("failed to connect database:", err)
		return
	}

	// 自動マイグレーションを実行することで、モデルに基づいたテーブルが作成される
	db.AutoMigrate(&Memo{})

	cmd := os.Args[1]
	switch cmd {
	case "add":
		if len(os.Args) < 3 {
			fmt.Println("Please provide memo to add")
			usage()
			os.Exit(1)
		}
		memoText := os.Args[2]
		addMemo(db, memoText)
	case "list":
		listMemos(db)
	case "delete":
		if len(os.Args) < 3 {
			fmt.Println("Please provide a memo ID to delete")
			usage()
			os.Exit(1)
		}
		idStr := os.Args[2]
		id, err := strconv.Atoi(idStr)
		if err != nil {
			fmt.Println("Invalid ID:", idStr)
			os.Exit(1)
		}
		deleteMemo(db, uint(id))
	default:
		fmt.Println("Invalid command")
		usage()
		os.Exit(1)
	}
}

type Memo struct {
	ID      uint   `gorm:"primaryKey"`
	Content string `gorm:"not null"`
}

func usage() {
	fmt.Println(`Usage:
	memo add <memo>
	memo list
	memo delete <memo ID>`)
}

func addMemo(db *gorm.DB, content string) {
	memo := Memo{Content: content}
	result := db.Create(&memo)
	if result.Error != nil {
		log.Fatal(result.Error)
	}
	fmt.Printf("Memo added successfully. ID: %d\n", memo.ID)
}

func listMemos(db *gorm.DB) {
	var memos []Memo
	result := db.Find(&memos)
	if result.Error != nil {
		log.Fatal(result.Error)
	}
	fmt.Println("Memo list:")
	for _, m := range memos {
		fmt.Printf("%d: %s\n", m.ID, m.Content)
	}
}

func deleteMemo(db *gorm.DB, id uint) {
	result := db.Delete(&Memo{}, id)
	if result.Error != nil {
		log.Fatal(result.Error)
	}
	if result.RowsAffected == 0 {
		fmt.Println("No memo found with given ID.")
	} else {
		fmt.Println("Memo deleted successfully.")
	}
}

この全体コードをビルドすると、以下の操作が可能になります。

  • ./memo add "メモ内容" でメモが追加される
  • ./memo list で追加されたメモの一覧が表示される
  • ./memo delete <ID> で指定したIDのメモが削除される

これで、GORMを使った基本的なCRUD操作の実装が完了しました。

7. まとめ 💥

今回の記事では、GolangとSQLiteを使ってシンプルなメモアプリを作成する方法を、直接SQLによる実装から一歩進んでORM(GORM)を利用した実装にリファクタリングする手順を紹介しました。
GORMを用いることで、モデル定義、マイグレーション、CRUD操作が非常に簡潔に実装でき、コードの保守性や可読性が向上します。
この記事のステップバイステップのアプローチを通じて、ORMのメリットを実感し、今後のアプリケーション開発に活かしていただければ幸いです!

Discussion