upper/db
upper/dbは「A productive data access layer for Go」として開発された、PostgreSQL、CockroachDB、MySQL、SQLite、MongoDBに対応するGoのデータアクセス層(DAL)ライブラリです。ORM的な機能を持ちながら、一般的なデータベース操作のためのツールを提供し、高度なシナリオでは目立たずに機能します。SQL、NoSQLデータベースに関係なく、統一されたAPIを通じて異なるデータソースと連携でき、生産性と柔軟性のバランスを重視した設計になっています。
GitHub概要
upper/db
Data Access Layer (DAL) for PostgreSQL, CockroachDB, MySQL, SQLite and MongoDB with ORM-like features.
トピックス
スター履歴
ライブラリ
upper/db
概要
upper/dbは「A productive data access layer for Go」として開発された、PostgreSQL、CockroachDB、MySQL、SQLite、MongoDBに対応するGoのデータアクセス層(DAL)ライブラリです。ORM的な機能を持ちながら、一般的なデータベース操作のためのツールを提供し、高度なシナリオでは目立たずに機能します。SQL、NoSQLデータベースに関係なく、統一されたAPIを通じて異なるデータソースと連携でき、生産性と柔軟性のバランスを重視した設計になっています。
詳細
upper/db v4は、数年間のGo、SQLデータベース、各種APIの本番運用経験を集約した成果物です。従来のバニティインポートパス(upper.io/db.v3)から、より親しみやすいgithub.com/upper/db/v4パスに移行しました。SQLビルダーによるカスタムSQLクエリの構築、db.RecordとStoreインターフェースを通じた高度なパターンの実装、マルチデータベース対応の統一APIなど、企業レベルのアプリケーション開発に必要な機能を包括的にサポートします。
主な特徴
- マルチデータベース対応: PostgreSQL、CockroachDB、MySQL、SQLite、MongoDB統一サポート
- 統一API: SQL、NoSQLに関係なく一貫したインターフェース
- SQLビルダー: 高度なケースに対応するカスタムクエリ構築
- ORM的機能: db.RecordとStoreインターフェースによる便利な抽象化
- 非侵入的設計: 高度なシナリオでは目立たずに機能
- 生産性重視: 開始から即座に生産的な開発が可能
メリット・デメリット
メリット
- 複数データベースの統一的な操作と開発効率の向上
- SQLとNoSQLの両方に対応する一貫したAPI設計
- 生SQLとSQLビルダーの選択的使い分けが可能
- シンプルで直感的なAPIによる学習コストの低減
- 企業レベルの本番運用実績による信頼性
- Goらしいシンプルで明確な設計思想
デメリット
- Go言語専用でマルチ言語対応なし
- 重厚なORM機能は提供されない軽量設計
- 複雑なリレーションシップ管理は手動実装が必要
- データベース固有の高度な機能は抽象化されない場合がある
- 大規模なレガシーシステムの移行には適さない可能性
- 完全なORM置き換えとしては機能が限定的
参考ページ
書き方の例
セットアップ
# Go モジュール初期化
go mod init upper-db-example
# upper/db v4 インストール
go get github.com/upper/db/v4
# データベースドライバー追加(使用するデータベースに応じて)
go get github.com/upper/db/v4/adapter/postgresql # PostgreSQL
go get github.com/upper/db/v4/adapter/mysql # MySQL
go get github.com/upper/db/v4/adapter/sqlite # SQLite
go get github.com/upper/db/v4/adapter/mongo # MongoDB
// go.mod
module upper-db-example
go 1.21
require (
github.com/upper/db/v4 v4.8.0
github.com/upper/db/v4/adapter/postgresql v4.8.0
github.com/upper/db/v4/adapter/mysql v4.8.0
github.com/upper/db/v4/adapter/sqlite v4.8.0
github.com/upper/db/v4/adapter/mongo v4.8.0
)
基本的な使い方
package main
import (
"fmt"
"log"
"time"
"github.com/upper/db/v4"
"github.com/upper/db/v4/adapter/postgresql"
"github.com/upper/db/v4/adapter/sqlite"
)
// User モデル
type User struct {
ID uint `db:"id,omitempty"`
Username string `db:"username"`
Email string `db:"email"`
CreatedAt time.Time `db:"created_at,omitempty"`
}
// Post モデル
type Post struct {
ID uint `db:"id,omitempty"`
Title string `db:"title"`
Content string `db:"content"`
AuthorID uint `db:"author_id"`
User *User `db:"-"` // リレーション用(DBフィールドではない)
}
func main() {
// PostgreSQL接続
postgresSession, err := postgresql.Open(postgresql.ConnectionURL{
Database: "testdb",
Host: "localhost",
User: "testuser",
Password: "testpass",
})
if err != nil {
log.Fatal("PostgreSQL接続エラー:", err)
}
defer postgresSession.Close()
// SQLite接続(比較用)
sqliteSession, err := sqlite.Open(sqlite.ConnectionURL{
Database: "test.db",
})
if err != nil {
log.Fatal("SQLite接続エラー:", err)
}
defer sqliteSession.Close()
// データベース操作の例
err = demonstrateBasicOperations(postgresSession)
if err != nil {
log.Fatal("操作エラー:", err)
}
}
func demonstrateBasicOperations(sess db.Session) error {
// テーブル参照取得
usersCol := sess.Collection("users")
postsCol := sess.Collection("posts")
// ユーザー作成
user := User{
Username: "john_doe",
Email: "[email protected]",
CreatedAt: time.Now(),
}
id, err := usersCol.Insert(user)
if err != nil {
return fmt.Errorf("ユーザー挿入エラー: %w", err)
}
fmt.Printf("作成されたユーザーID: %v\n", id)
// ユーザー検索
var foundUser User
err = usersCol.Find(db.Cond{"username": "john_doe"}).One(&foundUser)
if err != nil {
return fmt.Errorf("ユーザー検索エラー: %w", err)
}
fmt.Printf("検索されたユーザー: %+v\n", foundUser)
// 投稿作成
post := Post{
Title: "My First Post",
Content: "This is the content of my first post.",
AuthorID: foundUser.ID,
}
_, err = postsCol.Insert(post)
if err != nil {
return fmt.Errorf("投稿挿入エラー: %w", err)
}
// 複数ユーザー取得
var users []User
err = usersCol.Find().All(&users)
if err != nil {
return fmt.Errorf("ユーザー一覧取得エラー: %w", err)
}
fmt.Printf("全ユーザー数: %d\n", len(users))
return nil
}
SQLビルダーを使った高度なクエリ
package main
import (
"fmt"
"log"
"github.com/upper/db/v4"
"github.com/upper/db/v4/adapter/postgresql"
)
type UserWithPostCount struct {
ID uint `db:"id"`
Username string `db:"username"`
Email string `db:"email"`
PostCount int `db:"post_count"`
}
type PostWithAuthor struct {
ID uint `db:"id"`
Title string `db:"title"`
Content string `db:"content"`
AuthorName string `db:"author_name"`
AuthorEmail string `db:"author_email"`
}
func demonstrateSQLBuilder(sess db.Session) error {
// SQLビルダーの取得
sqlBuilder := sess.SQL()
// ユーザーと投稿数のJOINクエリ
var usersWithCounts []UserWithPostCount
err := sqlBuilder.
Select("u.id", "u.username", "u.email", "COUNT(p.id) as post_count").
From("users u").
LeftJoin("posts p ON u.id = p.author_id").
GroupBy("u.id", "u.username", "u.email").
OrderBy("post_count DESC").
All(&usersWithCounts)
if err != nil {
return fmt.Errorf("JOIN クエリエラー: %w", err)
}
fmt.Printf("ユーザーと投稿数:\n")
for _, user := range usersWithCounts {
fmt.Printf(" %s (%s): %d posts\n", user.Username, user.Email, user.PostCount)
}
// 投稿と著者情報のJOINクエリ
var postsWithAuthors []PostWithAuthor
err = sqlBuilder.
Select("p.id", "p.title", "p.content", "u.username as author_name", "u.email as author_email").
From("posts p").
Join("users u ON p.author_id = u.id").
Where("p.title LIKE ?", "%First%").
OrderBy("p.id DESC").
All(&postsWithAuthors)
if err != nil {
return fmt.Errorf("投稿JOIN クエリエラー: %w", err)
}
fmt.Printf("\n投稿と著者情報:\n")
for _, post := range postsWithAuthors {
fmt.Printf(" %s by %s (%s)\n", post.Title, post.AuthorName, post.AuthorEmail)
}
// 条件付き更新
res, err := sqlBuilder.
Update("users").
Set("email = ?", "[email protected]").
Where("username = ?", "john_doe").
Exec()
if err != nil {
return fmt.Errorf("更新エラー: %w", err)
}
rowsAffected, _ := res.RowsAffected()
fmt.Printf("\n更新された行数: %d\n", rowsAffected)
return nil
}
// 複雑な検索条件を使った例
func demonstrateAdvancedQueries(sess db.Session) error {
postsCol := sess.Collection("posts")
// 複雑な条件での検索
var posts []Post
err := postsCol.Find(
db.Or(
db.Cond{"title LIKE": "%Go%"},
db.Cond{"title LIKE": "%Database%"},
),
db.Cond{"author_id >": 0},
).OrderBy("-id").Limit(10).All(&posts)
if err != nil {
return fmt.Errorf("高度な検索エラー: %w", err)
}
fmt.Printf("検索結果: %d件\n", len(posts))
// ページネーション
const pageSize = 5
page := 2
var paginatedPosts []Post
err = postsCol.Find().
OrderBy("id").
Limit(pageSize).
Offset((page - 1) * pageSize).
All(&paginatedPosts)
if err != nil {
return fmt.Errorf("ページネーションエラー: %w", err)
}
fmt.Printf("ページ %d の投稿: %d件\n", page, len(paginatedPosts))
return nil
}
func main() {
sess, err := postgresql.Open(postgresql.ConnectionURL{
Database: "testdb",
Host: "localhost",
User: "testuser",
Password: "testpass",
})
if err != nil {
log.Fatal("接続エラー:", err)
}
defer sess.Close()
if err := demonstrateSQLBuilder(sess); err != nil {
log.Fatal("SQLビルダーエラー:", err)
}
if err := demonstrateAdvancedQueries(sess); err != nil {
log.Fatal("高度なクエリエラー:", err)
}
}
マルチデータベース対応とトランザクション
package main
import (
"fmt"
"log"
"github.com/upper/db/v4"
"github.com/upper/db/v4/adapter/postgresql"
"github.com/upper/db/v4/adapter/mysql"
"github.com/upper/db/v4/adapter/sqlite"
"github.com/upper/db/v4/adapter/mongo"
)
// データベース抽象化インターフェース
type DatabaseService interface {
CreateUser(user User) (interface{}, error)
GetUserByUsername(username string) (*User, error)
GetAllUsers() ([]User, error)
Close() error
}
// PostgreSQL実装
type PostgreSQLService struct {
session db.Session
}
func NewPostgreSQLService() (*PostgreSQLService, error) {
sess, err := postgresql.Open(postgresql.ConnectionURL{
Database: "testdb",
Host: "localhost",
User: "testuser",
Password: "testpass",
})
if err != nil {
return nil, err
}
return &PostgreSQLService{session: sess}, nil
}
func (s *PostgreSQLService) CreateUser(user User) (interface{}, error) {
return s.session.Collection("users").Insert(user)
}
func (s *PostgreSQLService) GetUserByUsername(username string) (*User, error) {
var user User
err := s.session.Collection("users").Find(db.Cond{"username": username}).One(&user)
if err != nil {
return nil, err
}
return &user, nil
}
func (s *PostgreSQLService) GetAllUsers() ([]User, error) {
var users []User
err := s.session.Collection("users").Find().All(&users)
return users, err
}
func (s *PostgreSQLService) Close() error {
return s.session.Close()
}
// MongoDB実装(NoSQL例)
type MongoService struct {
session db.Session
}
func NewMongoService() (*MongoService, error) {
sess, err := mongo.Open(mongo.ConnectionURL{
Database: "testdb",
Host: "localhost",
})
if err != nil {
return nil, err
}
return &MongoService{session: sess}, nil
}
func (s *MongoService) CreateUser(user User) (interface{}, error) {
return s.session.Collection("users").Insert(user)
}
func (s *MongoService) GetUserByUsername(username string) (*User, error) {
var user User
err := s.session.Collection("users").Find(db.Cond{"username": username}).One(&user)
if err != nil {
return nil, err
}
return &user, nil
}
func (s *MongoService) GetAllUsers() ([]User, error) {
var users []User
err := s.session.Collection("users").Find().All(&users)
return users, err
}
func (s *MongoService) Close() error {
return s.session.Close()
}
// トランザクション処理の例
func demonstrateTransactions(sess db.Session) error {
return sess.Tx(func(tx db.Session) error {
usersCol := tx.Collection("users")
postsCol := tx.Collection("posts")
// ユーザー作成
user := User{
Username: "tx_user",
Email: "[email protected]",
}
userID, err := usersCol.Insert(user)
if err != nil {
return fmt.Errorf("トランザクション内ユーザー作成エラー: %w", err)
}
// 投稿作成
post := Post{
Title: "Transaction Post",
Content: "Created within a transaction",
AuthorID: userID.(uint),
}
_, err = postsCol.Insert(post)
if err != nil {
return fmt.Errorf("トランザクション内投稿作成エラー: %w", err)
}
fmt.Println("トランザクション処理完了")
return nil
})
}
// バッチ処理の例
func demonstrateBatchOperations(sess db.Session) error {
usersCol := sess.Collection("users")
// バッチ挿入
users := []User{
{Username: "batch_user1", Email: "[email protected]"},
{Username: "batch_user2", Email: "[email protected]"},
{Username: "batch_user3", Email: "[email protected]"},
}
// 各ユーザーを挿入
for _, user := range users {
_, err := usersCol.Insert(user)
if err != nil {
return fmt.Errorf("バッチ挿入エラー: %w", err)
}
}
// バッチ更新
res, err := sess.SQL().
Update("users").
Set("email = CONCAT(username, '@batch.com')").
Where("username LIKE ?", "batch_%").
Exec()
if err != nil {
return fmt.Errorf("バッチ更新エラー: %w", err)
}
rowsAffected, _ := res.RowsAffected()
fmt.Printf("バッチ更新された行数: %d\n", rowsAffected)
return nil
}
func main() {
// PostgreSQL サービス
pgService, err := NewPostgreSQLService()
if err != nil {
log.Fatal("PostgreSQL サービス作成エラー:", err)
}
defer pgService.Close()
// 統一インターフェースでの操作
services := []DatabaseService{pgService}
for i, service := range services {
fmt.Printf("=== データベースサービス %d ===\n", i+1)
// ユーザー作成
user := User{
Username: fmt.Sprintf("service_user_%d", i+1),
Email: fmt.Sprintf("service%[email protected]", i+1),
}
_, err := service.CreateUser(user)
if err != nil {
log.Printf("ユーザー作成エラー: %v", err)
continue
}
// ユーザー取得
foundUser, err := service.GetUserByUsername(user.Username)
if err != nil {
log.Printf("ユーザー取得エラー: %v", err)
continue
}
fmt.Printf("作成・取得されたユーザー: %+v\n", foundUser)
}
// トランザクションとバッチ処理のデモ
if err := demonstrateTransactions(pgService.session); err != nil {
log.Printf("トランザクションエラー: %v", err)
}
if err := demonstrateBatchOperations(pgService.session); err != nil {
log.Printf("バッチ処理エラー: %v", err)
}
}