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