← レッスン一覧に戻る

解答 05.3 — 統合プロジェクト(classifyRegistrationError)¶

このノートブックは 05.3-integration-project.ipynb の練習問題の解答です。

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

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

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

	_ "modernc.org/sqlite"
)

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

// openCourseDB は、このコース共通の SQLite 接続を開く(1セル完結で使う。04.3 と同じ契約)。
func openCourseDB(path string) *sql.DB {
	db, err := sql.Open("sqlite", path)
	if err != nil {
		panic(err)
	}
	db.SetMaxOpenConns(1)
	return db
}

解答: classifyRegistrationError¶

nil はそのまま透過、UNIQUE制約違反は意味のあるエラーに変換、それ以外はそのまま返します。

In [2]:
func classifyRegistrationError(err error) error {
	if err == nil {
		return nil
	}
	if strings.Contains(err.Error(), "UNIQUE constraint failed") {
		return errors.New("email already registered")
	}
	return err
}

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

In [3]:
import "reflect"
import "fmt"

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 [4]:
%%
mustEqual(classifyRegistrationError(nil), nil, "nilエラーはnilのまま")

otherErr := errors.New("some other error")
mustEqual(classifyRegistrationError(otherErr), otherErr, "無関係なエラーはそのまま透過する")

dupSourceErr := errors.New("UNIQUE constraint failed: users.email")
classified := classifyRegistrationError(dupSourceErr)
if classified == nil || classified.Error() != "email already registered" {
	panic(fmt.Sprintf("❌ UNIQUE制約違反は email already registered に変換されるはず(got=%v)", classified))
}
fmt.Println("✅ Passed: UNIQUE制約違反は email already registered に変換される")

// 実際にDBへ重複INSERTして、本物のsqliteエラーでも判定できるか確認する。
db := openCourseDB("file:_05_3_exercise_check_solutions.db")
defer db.Close()
db.Exec(`DROP TABLE IF EXISTS exercise_users`)
db.Exec(`CREATE TABLE exercise_users (id INTEGER PRIMARY KEY, email TEXT UNIQUE)`)
db.Exec(`INSERT INTO exercise_users (email) VALUES (?)`, "hanako@example.com")
_, dupErr := db.Exec(`INSERT INTO exercise_users (email) VALUES (?)`, "hanako@example.com")

realClassified := classifyRegistrationError(dupErr)
if realClassified == nil || realClassified.Error() != "email already registered" {
	panic(fmt.Sprintf("❌ 実際のsqliteエラーも変換されるはず(got=%v)", realClassified))
}
fmt.Println("✅ Passed: 実際のsqlite UNIQUE制約違反も email already registered に変換される")

fmt.Println("🎉 すべてのチェックが通りました")
✅ Passed: nilエラーはnilのまま
✅ Passed: 無関係なエラーはそのまま透過する
✅ Passed: UNIQUE制約違反は email already registered に変換される
✅ Passed: 実際のsqlite UNIQUE制約違反も email already registered に変換される
🎉 すべてのチェックが通りました

解説¶

  1. nil を最初に処理する — 成功時(err == nil)はそのまま nil を返し、余計な変換をしません
  2. strings.Contains でSQLiteのエラーメッセージを判定する — modernc.org/sqlite が返す UNIQUE制約違反のエラーメッセージには "UNIQUE constraint failed" という共通の文字列が含まれます。 ①で実際にDBへ重複INSERTしたときのエラーも、この文字列を含んでいることをチェックセルで確認しています
  3. それ以外のエラーは握りつぶさずそのまま返す — DB接続エラーやSQL構文エラーなど、UNIQUE制約以外の エラーまで「登録に失敗しました」のような曖昧なメッセージに変換してしまうと、原因調査が難しくなります

この関数を実際のユーザー登録ハンドラに組み込めば、①で見た UNIQUE constraint failed という DB層の生エラーを、API層では 409 Conflict + "email already registered" という分かりやすい レスポンスに変換できます(このノートブックでは変換ロジックの実装までを扱い、ハンドラへの統合は 発展課題とします)。