← レッスン一覧に戻る

解答 04.3 — DAO層(TaskDAO.MarkDone)¶

このノートブックは 04.3-three-layer-mvc-dao.ipynb の練習問題の解答です。 まず問題の前提コードを再掲し、次に解答、最後にチェックを実行します。

先に自分の力で解いてから、答え合わせに使ってください。

前提コード(問題ノートブックと同じ定義)¶

In [1]:
import (
	"database/sql"
	"errors"
	"fmt"

	_ "modernc.org/sqlite"
)

// ErrUnanswered は、練習問題が未回答のときにプレースホルダ関数が返す特別なエラー。
var ErrUnanswered = errors.New("未回答: この関数はまだ実装されていません")

func openCourseDB(path string) *sql.DB {
	db, err := sql.Open("sqlite", path)
	if err != nil {
		panic(err)
	}
	db.SetMaxOpenConns(1)
	return db
}

const dbPath = "file:_04_3_three_layer_solutions.db"

type Task struct {
	ID    int64  `json:"id"`
	Title string `json:"title"`
	Done  bool   `json:"done"`
}

type TaskDAO struct{}

func (TaskDAO) Create(db *sql.DB, title string) (int64, error) {
	res, err := db.Exec(`INSERT INTO tasks (title, done) VALUES (?, 0)`, title)
	if err != nil {
		return 0, err
	}
	return res.LastInsertId()
}

func (TaskDAO) GetByID(db *sql.DB, id int64) (Task, error) {
	var t Task
	err := db.QueryRow(`SELECT id, title, done FROM tasks WHERE id = ?`, id).Scan(&t.ID, &t.Title, &t.Done)
	return t, err
}
In [2]:
%%
db := openCourseDB(dbPath)
defer db.Close()

db.Exec(`DROP TABLE IF EXISTS tasks`)
db.Exec(`CREATE TABLE tasks (id INTEGER PRIMARY KEY, title TEXT, done INTEGER)`)
fmt.Println("✅ tasks テーブルを初期化しました")
✅ tasks テーブルを初期化しました

解答: MarkDone¶

対象行が無くても db.Exec 自体はエラーになりません(sql.Result.RowsAffected() を見なければ 0件更新かどうかは分かりませんが、この仕様では確認不要です)。

In [3]:
func (TaskDAO) MarkDone(db *sql.DB, id int64) error {
	_, err := db.Exec(`UPDATE tasks SET done = 1 WHERE id = ?`, id)
	return err
}

チェック(問題ノートブックと同じ期待値)¶

答え合わせ用のヘルパー mustEqual を定義します。

In [4]:
import "reflect"

func mustEqual(got, want any, name string) {
	if reflect.DeepEqual(got, want) {
		fmt.Printf("✅ Passed: %s\n", name)
		return
	}
	panic(fmt.Sprintf("❌ %s\n  got  = %v (%T)\n  want = %v (%T)", name, got, got, want, want))
}
In [5]:
%%
db := openCourseDB(dbPath)
defer db.Close()

dao := TaskDAO{}
id, createErr := dao.Create(db, "牛乳を飲む")
if createErr != nil {
	panic(createErr)
}

err := dao.MarkDone(db, id)
mustEqual(err, nil, "MarkDone はエラーなし")

got, getErr := dao.GetByID(db, id)
mustEqual(getErr, nil, "GetByID はエラーなし")
mustEqual(got.Done, true, "MarkDone 後は done=true")

fmt.Println("🎉 すべてのチェックが通りました")
✅ Passed: MarkDone はエラーなし
✅ Passed: GetByID はエラーなし
✅ Passed: MarkDone 後は done=true
🎉 すべてのチェックが通りました

解説¶

  1. プレースホルダで安全に更新: WHERE id = ? により、id の値がどんな内容でも SQL構造には影響しません(04.4で学ぶSQLインジェクション対策と同じ考え方です)
  2. 対象行が無くてもエラーにしない: UPDATE文自体は「0行更新」で正常終了するため、 「そのIDが存在するか」を厳密にチェックしたい場合は別途 RowsAffected() を確認する必要が あります(このレッスンの仕様では省略しています)