sqlc
sqlcは「Generate type-safe Go from SQL」をコンセプトとする、Go専用のSQLクエリコンパイラです。生のSQLファイルから型安全なGoコードを自動生成し、コンパイル時にSQLの構文チェックと型検証を行います。ORMの抽象化レイヤーを介さず、SQLの表現力を最大限活用しながら、Goの型安全性とIDEサポートを提供します。データベーススキーマとSQLクエリの変更を自動検出し、対応するGoコードを再生成することで、SQLとGoコードの一貫性を常に保持し、実行時エラーを大幅に削減します。
GitHub概要
トピックス
スター履歴
ライブラリ
sqlc
概要
sqlcは「Generate type-safe Go from SQL」をコンセプトとする、Go専用のSQLクエリコンパイラです。生のSQLファイルから型安全なGoコードを自動生成し、コンパイル時にSQLの構文チェックと型検証を行います。ORMの抽象化レイヤーを介さず、SQLの表現力を最大限活用しながら、Goの型安全性とIDEサポートを提供します。データベーススキーマとSQLクエリの変更を自動検出し、対応するGoコードを再生成することで、SQLとGoコードの一貫性を常に保持し、実行時エラーを大幅に削減します。
詳細
sqlc 2025年版は、PostgreSQL、MySQL、SQLite、SQL Serverの完全サポートを提供し、各データベースの方言や固有機能を理解した精密なコード生成を実現します。Genericsを活用したモダンなGoコード生成、JSON・UUID・配列型などの高度なデータ型対応、プリペアドステートメントの自動最適化により、エンタープライズレベルのパフォーマンスを実現します。CIパイプライン統合、Docker対応、VS Code拡張による開発体験の向上により、チーム開発での生産性を最大化します。SQLファーストのアプローチにより、DBAとの連携や既存SQLの活用が容易です。
主な特徴
- 型安全コード生成: SQLクエリから完全型安全なGoコードを自動生成
- SQLファースト: 生SQLの表現力を損なわない開発アプローチ
- マルチDB対応: PostgreSQL、MySQL、SQLite、SQL Serverサポート
- コンパイル時検証: SQLの構文・型チェックをビルド時に実行
- 高性能: ORMオーバーヘッドなしのダイレクトSQL実行
- 開発者体験: VS Code統合、CIサポート、Dockerイメージ提供
メリット・デメリット
メリット
- SQLの表現力を完全に活用できる高度なクエリ記述
- コンパイル時型チェックによる実行時エラーの大幅削減
- ORMオーバーヘッドがない高いパフォーマンス
- 既存SQLクエリやDBAとの連携が容易
- 生成されるGoコードの可読性と保守性が高い
- 複数データベースの統一的な扱いが可能
デメリット
- Go言語専用でマルチ言語対応なし
- コード生成ステップが必要でビルドプロセスが複雑化
- シンプルなCRUD操作には過剰で開発初期のオーバーヘッド
- 動的クエリ構築には不向き
- ORM的なリレーションシップ機能は提供されない
- Active Recordパターンに慣れた開発者には学習障壁
参考ページ
書き方の例
セットアップ
# sqlcインストール
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
# プロジェクト初期化
mkdir sqlc-example
cd sqlc-example
go mod init sqlc-example
# sqlc設定ファイル作成
sqlc init
# sqlc.yaml
version: "2"
project:
id: "sqlc-example"
sql:
- engine: "postgresql"
queries: "sql/queries/"
schema: "sql/schema/"
gen:
go:
package: "db"
out: "internal/db"
sql_package: "pgx/v5"
emit_json_tags: true
emit_prepared_queries: true
emit_interface: true
emit_exact_table_names: true
emit_empty_slices: true
overrides:
- db_type: "pg_catalog.varchar"
go_type: "string"
- db_type: "uuid"
go_type: "github.com/google/uuid.UUID"
# 依存関係追加
go get github.com/jackc/pgx/v5
go get github.com/jackc/pgx/v5/pgxpool
go get github.com/google/uuid
スキーマとクエリ定義
-- sql/schema/001_init.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
age INTEGER NOT NULL CHECK (age >= 0),
active BOOLEAN NOT NULL DEFAULT true,
profile JSONB,
tags TEXT[],
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
published BOOLEAN NOT NULL DEFAULT false,
published_at TIMESTAMP,
view_count INTEGER NOT NULL DEFAULT 0,
tags TEXT[],
metadata JSONB,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_active ON users(active);
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_published ON posts(published);
CREATE INDEX idx_posts_published_at ON posts(published_at);
-- トリガー関数(更新時刻自動更新)
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER users_update_updated_at
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER posts_update_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW
EXECUTE FUNCTION update_updated_at();
-- sql/queries/users.sql
-- name: CreateUser :one
INSERT INTO users (name, email, age, profile, tags)
VALUES ($1, $2, $3, $4, $5)
RETURNING *;
-- name: GetUser :one
SELECT * FROM users
WHERE id = $1;
-- name: GetUserByEmail :one
SELECT * FROM users
WHERE email = $1;
-- name: ListUsers :many
SELECT * FROM users
ORDER BY created_at DESC
LIMIT $1 OFFSET $2;
-- name: ListActiveUsers :many
SELECT * FROM users
WHERE active = true
ORDER BY name;
-- name: SearchUsersByName :many
SELECT * FROM users
WHERE name ILIKE '%' || $1 || '%'
ORDER BY name
LIMIT $2;
-- name: UpdateUser :one
UPDATE users
SET name = $2, email = $3, age = $4, profile = $5, tags = $6
WHERE id = $1
RETURNING *;
-- name: UpdateUserActive :exec
UPDATE users
SET active = $2
WHERE id = $1;
-- name: DeleteUser :exec
DELETE FROM users
WHERE id = $1;
-- name: CountUsers :one
SELECT COUNT(*) FROM users;
-- name: CountActiveUsers :one
SELECT COUNT(*) FROM users
WHERE active = true;
-- name: GetUsersByAgeRange :many
SELECT * FROM users
WHERE age BETWEEN $1 AND $2
ORDER BY age, name;
-- name: BatchUpdateUserAges :exec
UPDATE users
SET age = age + $1
WHERE id = ANY($2::bigint[]);
-- sql/queries/posts.sql
-- name: CreatePost :one
INSERT INTO posts (title, content, user_id, tags, metadata)
VALUES ($1, $2, $3, $4, $5)
RETURNING *;
-- name: GetPost :one
SELECT * FROM posts
WHERE id = $1;
-- name: ListPosts :many
SELECT * FROM posts
ORDER BY created_at DESC
LIMIT $1 OFFSET $2;
-- name: ListPostsByUser :many
SELECT * FROM posts
WHERE user_id = $1
ORDER BY created_at DESC;
-- name: ListPublishedPosts :many
SELECT * FROM posts
WHERE published = true
ORDER BY published_at DESC
LIMIT $1 OFFSET $2;
-- name: GetPostsWithUsers :many
SELECT p.*, u.name as author_name, u.email as author_email
FROM posts p
JOIN users u ON p.user_id = u.id
ORDER BY p.created_at DESC
LIMIT $1 OFFSET $2;
-- name: SearchPosts :many
SELECT * FROM posts
WHERE title ILIKE '%' || $1 || '%'
OR content ILIKE '%' || $1 || '%'
ORDER BY created_at DESC
LIMIT $2;
-- name: UpdatePost :one
UPDATE posts
SET title = $2, content = $3, tags = $4, metadata = $5
WHERE id = $1
RETURNING *;
-- name: PublishPost :one
UPDATE posts
SET published = true, published_at = CURRENT_TIMESTAMP
WHERE id = $1
RETURNING *;
-- name: UnpublishPost :one
UPDATE posts
SET published = false, published_at = NULL
WHERE id = $1
RETURNING *;
-- name: IncrementViewCount :exec
UPDATE posts
SET view_count = view_count + 1
WHERE id = $1;
-- name: DeletePost :exec
DELETE FROM posts
WHERE id = $1;
-- name: DeletePostsByUser :exec
DELETE FROM posts
WHERE user_id = $1;
-- name: GetPopularPosts :many
SELECT * FROM posts
WHERE published = true
ORDER BY view_count DESC
LIMIT $1;
-- name: GetPostStatistics :one
SELECT
COUNT(*) as total_posts,
COUNT(*) FILTER (WHERE published = true) as published_posts,
AVG(view_count) as avg_view_count,
MAX(view_count) as max_view_count
FROM posts;
-- name: GetUserPostCounts :many
SELECT
u.id,
u.name,
COUNT(p.id) as post_count,
COUNT(p.id) FILTER (WHERE p.published = true) as published_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
GROUP BY u.id, u.name
ORDER BY post_count DESC;
コード生成と基本使用法
// internal/db/models.go (生成されるファイル)
package db
import (
"database/sql/driver"
"encoding/json"
"time"
"github.com/google/uuid"
"github.com/lib/pq"
)
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age int32 `json:"age"`
Active bool `json:"active"`
Profile json.RawMessage `json:"profile"`
Tags pq.StringArray `json:"tags"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type Post struct {
ID int64 `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
UserID int64 `json:"user_id"`
Published bool `json:"published"`
PublishedAt *time.Time `json:"published_at"`
ViewCount int32 `json:"view_count"`
Tags pq.StringArray `json:"tags"`
Metadata json.RawMessage `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
# コード生成実行
sqlc generate
// main.go
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/lib/pq"
"sqlc-example/internal/db"
)
// データベース接続
func connectDB() (*pgxpool.Pool, error) {
databaseURL := os.Getenv("DATABASE_URL")
if databaseURL == "" {
databaseURL = "postgresql://postgres:password@localhost:5432/sqlc_example?sslmode=disable"
}
config, err := pgxpool.ParseConfig(databaseURL)
if err != nil {
return nil, fmt.Errorf("failed to parse config: %w", err)
}
pool, err := pgxpool.NewWithConfig(context.Background(), config)
if err != nil {
return nil, fmt.Errorf("failed to create connection pool: %w", err)
}
return pool, nil
}
// ユーザーサービス
type UserService struct {
queries *db.Queries
}
func NewUserService(pool *pgxpool.Pool) *UserService {
return &UserService{
queries: db.New(pool),
}
}
func (s *UserService) CreateUser(ctx context.Context, name, email string, age int32, profile map[string]interface{}, tags []string) (*db.User, error) {
// プロフィールをJSONに変換
profileJSON, err := json.Marshal(profile)
if err != nil {
return nil, fmt.Errorf("failed to marshal profile: %w", err)
}
user, err := s.queries.CreateUser(ctx, db.CreateUserParams{
Name: name,
Email: email,
Age: age,
Profile: profileJSON,
Tags: pq.StringArray(tags),
})
if err != nil {
return nil, fmt.Errorf("failed to create user: %w", err)
}
return &user, nil
}
func (s *UserService) GetUser(ctx context.Context, id int64) (*db.User, error) {
user, err := s.queries.GetUser(ctx, id)
if err != nil {
if err == pgx.ErrNoRows {
return nil, fmt.Errorf("user not found: %d", id)
}
return nil, fmt.Errorf("failed to get user: %w", err)
}
return &user, nil
}
func (s *UserService) UpdateUser(ctx context.Context, id int64, name, email string, age int32, profile map[string]interface{}, tags []string) (*db.User, error) {
profileJSON, err := json.Marshal(profile)
if err != nil {
return nil, fmt.Errorf("failed to marshal profile: %w", err)
}
user, err := s.queries.UpdateUser(ctx, db.UpdateUserParams{
ID: id,
Name: name,
Email: email,
Age: age,
Profile: profileJSON,
Tags: pq.StringArray(tags),
})
if err != nil {
return nil, fmt.Errorf("failed to update user: %w", err)
}
return &user, nil
}
func (s *UserService) ListUsers(ctx context.Context, limit, offset int32) ([]db.User, error) {
users, err := s.queries.ListUsers(ctx, db.ListUsersParams{
Limit: limit,
Offset: offset,
})
if err != nil {
return nil, fmt.Errorf("failed to list users: %w", err)
}
return users, nil
}
func (s *UserService) SearchUsers(ctx context.Context, query string, limit int32) ([]db.User, error) {
users, err := s.queries.SearchUsersByName(ctx, db.SearchUsersByNameParams{
Name: query,
Limit: limit,
})
if err != nil {
return nil, fmt.Errorf("failed to search users: %w", err)
}
return users, nil
}
func (s *UserService) GetUsersByAgeRange(ctx context.Context, minAge, maxAge int32) ([]db.User, error) {
users, err := s.queries.GetUsersByAgeRange(ctx, db.GetUsersByAgeRangeParams{
Age: minAge,
Age_2: maxAge,
})
if err != nil {
return nil, fmt.Errorf("failed to get users by age range: %w", err)
}
return users, nil
}
func (s *UserService) DeleteUser(ctx context.Context, id int64) error {
err := s.queries.DeleteUser(ctx, id)
if err != nil {
return fmt.Errorf("failed to delete user: %w", err)
}
return nil
}
// 使用例
func demonstrateBasicOperations() {
ctx := context.Background()
// データベース接続
pool, err := connectDB()
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
defer pool.Close()
userService := NewUserService(pool)
// ユーザー作成
profile := map[string]interface{}{
"bio": "Software Developer",
"location": "Tokyo, Japan",
"website": "https://example.com",
}
tags := []string{"developer", "go", "postgres"}
user, err := userService.CreateUser(ctx, "Alice Johnson", "[email protected]", 28, profile, tags)
if err != nil {
log.Printf("Failed to create user: %v", err)
return
}
fmt.Printf("Created user: %+v\n", user)
// ユーザー取得
retrievedUser, err := userService.GetUser(ctx, user.ID)
if err != nil {
log.Printf("Failed to get user: %v", err)
return
}
fmt.Printf("Retrieved user: %+v\n", retrievedUser)
// ユーザー更新
updatedProfile := map[string]interface{}{
"bio": "Senior Software Developer",
"location": "Tokyo, Japan",
"website": "https://alice-dev.com",
"skills": []string{"Go", "PostgreSQL", "Docker"},
}
updatedTags := []string{"senior-developer", "go", "postgres", "docker"}
updatedUser, err := userService.UpdateUser(ctx, user.ID, "Alice Johnson Smith", "[email protected]", 29, updatedProfile, updatedTags)
if err != nil {
log.Printf("Failed to update user: %v", err)
return
}
fmt.Printf("Updated user: %+v\n", updatedUser)
// ユーザー検索
searchResults, err := userService.SearchUsers(ctx, "Alice", 10)
if err != nil {
log.Printf("Failed to search users: %v", err)
return
}
fmt.Printf("Search results: %+v\n", searchResults)
// 年齢範囲でフィルタ
ageRangeUsers, err := userService.GetUsersByAgeRange(ctx, 25, 35)
if err != nil {
log.Printf("Failed to get users by age range: %v", err)
return
}
fmt.Printf("Users in age range 25-35: %+v\n", ageRangeUsers)
}
投稿機能とJOINクエリ
// ポストサービス
type PostService struct {
queries *db.Queries
}
func NewPostService(pool *pgxpool.Pool) *PostService {
return &PostService{
queries: db.New(pool),
}
}
func (s *PostService) CreatePost(ctx context.Context, title, content string, userID int64, tags []string, metadata map[string]interface{}) (*db.Post, error) {
metadataJSON, err := json.Marshal(metadata)
if err != nil {
return nil, fmt.Errorf("failed to marshal metadata: %w", err)
}
post, err := s.queries.CreatePost(ctx, db.CreatePostParams{
Title: title,
Content: content,
UserID: userID,
Tags: pq.StringArray(tags),
Metadata: metadataJSON,
})
if err != nil {
return nil, fmt.Errorf("failed to create post: %w", err)
}
return &post, nil
}
func (s *PostService) GetPost(ctx context.Context, id int64) (*db.Post, error) {
post, err := s.queries.GetPost(ctx, id)
if err != nil {
if err == pgx.ErrNoRows {
return nil, fmt.Errorf("post not found: %d", id)
}
return nil, fmt.Errorf("failed to get post: %w", err)
}
return &post, nil
}
func (s *PostService) ListPostsByUser(ctx context.Context, userID int64) ([]db.Post, error) {
posts, err := s.queries.ListPostsByUser(ctx, userID)
if err != nil {
return nil, fmt.Errorf("failed to list posts by user: %w", err)
}
return posts, nil
}
func (s *PostService) GetPostsWithUsers(ctx context.Context, limit, offset int32) ([]db.GetPostsWithUsersRow, error) {
posts, err := s.queries.GetPostsWithUsers(ctx, db.GetPostsWithUsersParams{
Limit: limit,
Offset: offset,
})
if err != nil {
return nil, fmt.Errorf("failed to get posts with users: %w", err)
}
return posts, nil
}
func (s *PostService) PublishPost(ctx context.Context, id int64) (*db.Post, error) {
post, err := s.queries.PublishPost(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to publish post: %w", err)
}
return &post, nil
}
func (s *PostService) IncrementViewCount(ctx context.Context, id int64) error {
err := s.queries.IncrementViewCount(ctx, id)
if err != nil {
return fmt.Errorf("failed to increment view count: %w", err)
}
return nil
}
func (s *PostService) GetPopularPosts(ctx context.Context, limit int32) ([]db.Post, error) {
posts, err := s.queries.GetPopularPosts(ctx, limit)
if err != nil {
return nil, fmt.Errorf("failed to get popular posts: %w", err)
}
return posts, nil
}
func (s *PostService) GetPostStatistics(ctx context.Context) (*db.GetPostStatisticsRow, error) {
stats, err := s.queries.GetPostStatistics(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get post statistics: %w", err)
}
return &stats, nil
}
func (s *PostService) GetUserPostCounts(ctx context.Context) ([]db.GetUserPostCountsRow, error) {
counts, err := s.queries.GetUserPostCounts(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get user post counts: %w", err)
}
return counts, nil
}
// 使用例
func demonstratePostOperations() {
ctx := context.Background()
pool, err := connectDB()
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
defer pool.Close()
userService := NewUserService(pool)
postService := NewPostService(pool)
// ユーザー作成
user, err := userService.CreateUser(ctx, "John Doe", "[email protected]", 30, map[string]interface{}{}, []string{"author"})
if err != nil {
log.Printf("Failed to create user: %v", err)
return
}
// 投稿作成
metadata := map[string]interface{}{
"category": "technology",
"readTime": 5,
"featured": true,
}
tags := []string{"go", "programming", "tutorial"}
post, err := postService.CreatePost(ctx, "Getting Started with Go", "This is a comprehensive guide to Go programming...", user.ID, tags, metadata)
if err != nil {
log.Printf("Failed to create post: %v", err)
return
}
fmt.Printf("Created post: %+v\n", post)
// 投稿公開
publishedPost, err := postService.PublishPost(ctx, post.ID)
if err != nil {
log.Printf("Failed to publish post: %v", err)
return
}
fmt.Printf("Published post: %+v\n", publishedPost)
// ビューカウント増加
err = postService.IncrementViewCount(ctx, post.ID)
if err != nil {
log.Printf("Failed to increment view count: %v", err)
return
}
// 投稿とユーザー情報を結合取得
postsWithUsers, err := postService.GetPostsWithUsers(ctx, 10, 0)
if err != nil {
log.Printf("Failed to get posts with users: %v", err)
return
}
fmt.Printf("Posts with users: %+v\n", postsWithUsers)
// 統計情報取得
stats, err := postService.GetPostStatistics(ctx)
if err != nil {
log.Printf("Failed to get statistics: %v", err)
return
}
fmt.Printf("Post statistics: %+v\n", stats)
// ユーザー別投稿数
userPostCounts, err := postService.GetUserPostCounts(ctx)
if err != nil {
log.Printf("Failed to get user post counts: %v", err)
return
}
fmt.Printf("User post counts: %+v\n", userPostCounts)
}
トランザクションと高度な機能
// トランザクションサービス
type TransactionService struct {
pool *pgxpool.Pool
}
func NewTransactionService(pool *pgxpool.Pool) *TransactionService {
return &TransactionService{pool: pool}
}
// ユーザーと初期投稿を同時作成
func (s *TransactionService) CreateUserWithPosts(ctx context.Context, userName, userEmail string, userAge int32, initialPosts []struct {
Title string
Content string
Tags []string
}) (*db.User, []db.Post, error) {
tx, err := s.pool.Begin(ctx)
if err != nil {
return nil, nil, fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx)
queries := db.New(tx)
// ユーザー作成
user, err := queries.CreateUser(ctx, db.CreateUserParams{
Name: userName,
Email: userEmail,
Age: userAge,
Profile: json.RawMessage("{}"),
Tags: pq.StringArray{},
})
if err != nil {
return nil, nil, fmt.Errorf("failed to create user: %w", err)
}
// 初期投稿作成
var posts []db.Post
for _, postData := range initialPosts {
post, err := queries.CreatePost(ctx, db.CreatePostParams{
Title: postData.Title,
Content: postData.Content,
UserID: user.ID,
Tags: pq.StringArray(postData.Tags),
Metadata: json.RawMessage("{}"),
})
if err != nil {
return nil, nil, fmt.Errorf("failed to create post: %w", err)
}
posts = append(posts, post)
}
if err = tx.Commit(ctx); err != nil {
return nil, nil, fmt.Errorf("failed to commit transaction: %w", err)
}
return &user, posts, nil
}
// ユーザー間で投稿を移譲
func (s *TransactionService) TransferPosts(ctx context.Context, fromUserID, toUserID int64) error {
tx, err := s.pool.Begin(ctx)
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx)
queries := db.New(tx)
// 両方のユーザーの存在確認
_, err = queries.GetUser(ctx, fromUserID)
if err != nil {
return fmt.Errorf("source user not found: %w", err)
}
_, err = queries.GetUser(ctx, toUserID)
if err != nil {
return fmt.Errorf("target user not found: %w", err)
}
// 投稿の移譲(カスタムクエリ)
_, err = tx.Exec(ctx, "UPDATE posts SET user_id = $1 WHERE user_id = $2", toUserID, fromUserID)
if err != nil {
return fmt.Errorf("failed to transfer posts: %w", err)
}
if err = tx.Commit(ctx); err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}
return nil
}
// バッチ処理:複数ユーザーの年齢を一括更新
func (s *TransactionService) BatchUpdateAges(ctx context.Context, ageIncrement int32, userIDs []int64) error {
tx, err := s.pool.Begin(ctx)
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx)
queries := db.New(tx)
err = queries.BatchUpdateUserAges(ctx, db.BatchUpdateUserAgesParams{
Age: ageIncrement,
Column: userIDs,
})
if err != nil {
return fmt.Errorf("failed to batch update ages: %w", err)
}
if err = tx.Commit(ctx); err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}
return nil
}
// 使用例
func demonstrateTransactions() {
ctx := context.Background()
pool, err := connectDB()
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
defer pool.Close()
txService := NewTransactionService(pool)
// ユーザーと初期投稿を同時作成
initialPosts := []struct {
Title string
Content string
Tags []string
}{
{"Welcome Post", "Welcome to my blog!", []string{"welcome", "introduction"}},
{"About Me", "Let me introduce myself...", []string{"about", "personal"}},
{"My First Tutorial", "This is my first technical post.", []string{"tutorial", "programming"}},
}
user, posts, err := txService.CreateUserWithPosts(ctx, "Content Creator", "[email protected]", 25, initialPosts)
if err != nil {
log.Printf("Failed to create user with posts: %v", err)
return
}
fmt.Printf("Created user: %+v\n", user)
fmt.Printf("Created %d posts\n", len(posts))
// 別のユーザー作成
userService := NewUserService(pool)
targetUser, err := userService.CreateUser(ctx, "Another User", "[email protected]", 30, map[string]interface{}{}, []string{})
if err != nil {
log.Printf("Failed to create target user: %v", err)
return
}
// 投稿移譲
err = txService.TransferPosts(ctx, user.ID, targetUser.ID)
if err != nil {
log.Printf("Failed to transfer posts: %v", err)
return
}
fmt.Printf("Transferred posts from user %d to user %d\n", user.ID, targetUser.ID)
// バッチ年齢更新
userIDs := []int64{user.ID, targetUser.ID}
err = txService.BatchUpdateAges(ctx, 1, userIDs)
if err != nil {
log.Printf("Failed to batch update ages: %v", err)
return
}
fmt.Printf("Updated ages for users: %v\n", userIDs)
}
エラーハンドリングと実用的なパターン
// カスタムエラー型
type AppError struct {
Code string
Message string
Err error
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("%s: %s (%v)", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error {
return e.Err
}
// エラーヘルパー関数
func NewAppError(code, message string, err error) *AppError {
return &AppError{
Code: code,
Message: message,
Err: err,
}
}
func IsNotFoundError(err error) bool {
return err == pgx.ErrNoRows
}
// 安全なサービス実装
type SafeUserService struct {
queries *db.Queries
}
func NewSafeUserService(pool *pgxpool.Pool) *SafeUserService {
return &SafeUserService{
queries: db.New(pool),
}
}
func (s *SafeUserService) CreateUserSafely(ctx context.Context, name, email string, age int32) (*db.User, error) {
// バリデーション
if name == "" {
return nil, NewAppError("VALIDATION_ERROR", "Name cannot be empty", nil)
}
if email == "" {
return nil, NewAppError("VALIDATION_ERROR", "Email cannot be empty", nil)
}
if age < 0 || age > 150 {
return nil, NewAppError("VALIDATION_ERROR", "Age must be between 0 and 150", nil)
}
// 重複チェック
_, err := s.queries.GetUserByEmail(ctx, email)
if err == nil {
return nil, NewAppError("DUPLICATE_EMAIL", "Email already exists", nil)
}
if !IsNotFoundError(err) {
return nil, NewAppError("DATABASE_ERROR", "Failed to check email uniqueness", err)
}
// ユーザー作成
user, err := s.queries.CreateUser(ctx, db.CreateUserParams{
Name: name,
Email: email,
Age: age,
Profile: json.RawMessage("{}"),
Tags: pq.StringArray{},
})
if err != nil {
return nil, NewAppError("CREATE_ERROR", "Failed to create user", err)
}
return &user, nil
}
func (s *SafeUserService) GetUserSafely(ctx context.Context, id int64) (*db.User, error) {
user, err := s.queries.GetUser(ctx, id)
if err != nil {
if IsNotFoundError(err) {
return nil, NewAppError("NOT_FOUND", fmt.Sprintf("User not found: %d", id), err)
}
return nil, NewAppError("DATABASE_ERROR", "Failed to get user", err)
}
return &user, nil
}
func (s *SafeUserService) UpdateUserSafely(ctx context.Context, id int64, name, email string, age int32) (*db.User, error) {
// 存在確認
_, err := s.GetUserSafely(ctx, id)
if err != nil {
return nil, err
}
// バリデーション
if name == "" {
return nil, NewAppError("VALIDATION_ERROR", "Name cannot be empty", nil)
}
if email == "" {
return nil, NewAppError("VALIDATION_ERROR", "Email cannot be empty", nil)
}
// メール重複チェック(自分以外)
existingUser, err := s.queries.GetUserByEmail(ctx, email)
if err == nil && existingUser.ID != id {
return nil, NewAppError("DUPLICATE_EMAIL", "Email already exists", nil)
}
if err != nil && !IsNotFoundError(err) {
return nil, NewAppError("DATABASE_ERROR", "Failed to check email uniqueness", err)
}
// 更新実行
user, err := s.queries.UpdateUser(ctx, db.UpdateUserParams{
ID: id,
Name: name,
Email: email,
Age: age,
Profile: json.RawMessage("{}"),
Tags: pq.StringArray{},
})
if err != nil {
return nil, NewAppError("UPDATE_ERROR", "Failed to update user", err)
}
return &user, nil
}
// 接続プールヘルスチェック
func (s *SafeUserService) HealthCheck(ctx context.Context) error {
_, err := s.queries.CountUsers(ctx)
if err != nil {
return NewAppError("HEALTH_CHECK_ERROR", "Database connection failed", err)
}
return nil
}
// 使用例とエラーハンドリング
func demonstrateErrorHandling() {
ctx := context.Background()
pool, err := connectDB()
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
defer pool.Close()
safeService := NewSafeUserService(pool)
// ヘルスチェック
if err := safeService.HealthCheck(ctx); err != nil {
log.Printf("Health check failed: %v", err)
return
}
fmt.Println("✓ Database health check passed")
// 正常なユーザー作成
user, err := safeService.CreateUserSafely(ctx, "Jane Doe", "[email protected]", 28)
if err != nil {
var appErr *AppError
if errors.As(err, &appErr) {
log.Printf("Application error [%s]: %s", appErr.Code, appErr.Message)
} else {
log.Printf("Unexpected error: %v", err)
}
return
}
fmt.Printf("✓ Successfully created user: %+v\n", user)
// バリデーションエラーのテスト
_, err = safeService.CreateUserSafely(ctx, "", "invalid-email", -5)
if err != nil {
var appErr *AppError
if errors.As(err, &appErr) {
fmt.Printf("✓ Correctly caught validation error [%s]: %s\n", appErr.Code, appErr.Message)
}
}
// 重複メールエラーのテスト
_, err = safeService.CreateUserSafely(ctx, "Another Jane", "[email protected]", 30)
if err != nil {
var appErr *AppError
if errors.As(err, &appErr) {
fmt.Printf("✓ Correctly caught duplicate email error [%s]: %s\n", appErr.Code, appErr.Message)
}
}
// 存在しないユーザーの取得テスト
_, err = safeService.GetUserSafely(ctx, 999999)
if err != nil {
var appErr *AppError
if errors.As(err, &appErr) {
fmt.Printf("✓ Correctly caught not found error [%s]: %s\n", appErr.Code, appErr.Message)
}
}
// 正常な更新
updatedUser, err := safeService.UpdateUserSafely(ctx, user.ID, "Jane Smith", "[email protected]", 29)
if err != nil {
log.Printf("Update failed: %v", err)
return
}
fmt.Printf("✓ Successfully updated user: %+v\n", updatedUser)
}
// メイン関数
func main() {
fmt.Println("=== sqlc Demo ===\n")
demonstrateBasicOperations()
fmt.Println("\n" + strings.Repeat("=", 50) + "\n")
demonstratePostOperations()
fmt.Println("\n" + strings.Repeat("=", 50) + "\n")
demonstrateTransactions()
fmt.Println("\n" + strings.Repeat("=", 50) + "\n")
demonstrateErrorHandling()
}