← レッスン一覧に戻る

解答 02.4 — データモデリングと正規化(customerOrderRowCount)¶

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

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

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

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

	_ "modernc.org/sqlite"
)

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

const flatDBPath = "file:_02_4_normalization_solutions.db"
In [2]:
%%
db := openCourseDB(flatDBPath)
defer db.Close()

db.Exec(`DROP TABLE IF EXISTS orders_flat`)
db.Exec(`CREATE TABLE orders_flat (
	order_id INTEGER PRIMARY KEY,
	customer_id INTEGER,
	customer_name TEXT,
	customer_email TEXT,
	product_id INTEGER,
	product_name TEXT,
	price INTEGER,
	qty INTEGER
)`)

seed := []struct {
	customerID          int
	customerName, email string
	productID           int
	productName          string
	price, qty          int
}{
	{1, "佐藤", "sato@example.com", 10, "マグカップ", 800, 2},
	{1, "佐藤", "sato@example.com", 20, "ノート", 300, 1},
	{1, "佐藤", "sato@example.com", 30, "ペン", 150, 5},
	{2, "鈴木", "suzuki@example.com", 10, "マグカップ", 800, 3},
	{2, "鈴木", "suzuki@example.com", 20, "ノート", 300, 2},
	{3, "田中", "tanaka@example.com", 30, "ペン", 150, 1},
}
for _, s := range seed {
	_, err := db.Exec(
		`INSERT INTO orders_flat (customer_id, customer_name, customer_email, product_id, product_name, price, qty)
		 VALUES (?, ?, ?, ?, ?, ?, ?)`,
		s.customerID, s.customerName, s.email, s.productID, s.productName, s.price, s.qty)
	if err != nil {
		panic(err)
	}
}
fmt.Println("✅ orders_flat をシードしました(6行)")
✅ orders_flat をシードしました(6行)

解答: customerOrderRowCount¶

SELECT COUNT(*) ... WHERE customer_id = ? を QueryRow して Scan するだけです。 該当行が無ければ COUNT(*) は 0 を返す(SQL の性質上エラーにならない)ので、 「0件」と「エラー」を区別するコードは不要です。

In [3]:
func customerOrderRowCount(db *sql.DB, customerID int) (int, error) {
	var n int
	err := db.QueryRow(`SELECT COUNT(*) FROM orders_flat WHERE customer_id = ?`, customerID).Scan(&n)
	if err != nil {
		return 0, err
	}
	return n, nil
}

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

答え合わせ用のヘルパー 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]:
%%
// このチェックは SELECT のみで DB を変更しないため、何度実行しても同じ結果になる。
db := openCourseDB(flatDBPath)
defer db.Close()

n1, err1 := customerOrderRowCount(db, 1)
mustEqual(err1, nil, "customer_id=1 はエラーなし")
mustEqual(n1, 3, "佐藤さん(customer_id=1)は3行")

n2, err2 := customerOrderRowCount(db, 2)
mustEqual(err2, nil, "customer_id=2 はエラーなし")
mustEqual(n2, 2, "鈴木さん(customer_id=2)は2行")

n3, err3 := customerOrderRowCount(db, 999)
mustEqual(err3, nil, "存在しないIDはエラーではない")
mustEqual(n3, 0, "存在しないIDは0行")

fmt.Println("🎉 すべてのチェックが通りました")
✅ Passed: customer_id=1 はエラーなし
✅ Passed: 佐藤さん(customer_id=1)は3行
✅ Passed: customer_id=2 はエラーなし
✅ Passed: 鈴木さん(customer_id=2)は2行
✅ Passed: 存在しないIDはエラーではない
✅ Passed: 存在しないIDは0行
🎉 すべてのチェックが通りました

解説¶

  1. COUNT(*) は「0件」もエラーなく返す: SQL の集計関数は対象行が無くても 0 という 正しい値を返します。「見つからない」を error にする必要がある QueryRow の単一行取得 (例: SELECT name FROM ... WHERE id = ? で該当行が無いと sql.ErrNoRows)とは対照的です
  2. SELECT のみ = 何度実行しても安全: この関数は DB を変更しないため、チェックセルを 何度実行しても同じ結果になります(DBレッスンの練習問題チェックが SELECT 系を基本にする理由)
  3. この数値の意味: customerOrderRowCount(db, 1) が返す 3 は、02.4 本編で見た 「非正規化テーブルでは佐藤さんのメール変更が3行に波及する」の根拠そのものです