Initial commit
This commit is contained in:
725
examples/advanced-queries.go
Normal file
725
examples/advanced-queries.go
Normal file
@@ -0,0 +1,725 @@
|
||||
// Package examples demonstrates advanced query patterns with cool-mysql
|
||||
package examples
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
mysql "github.com/StirlingMarketingGroup/cool-mysql"
|
||||
)
|
||||
|
||||
// AdvancedQueryExamples demonstrates advanced query patterns
|
||||
func AdvancedQueryExamples() {
|
||||
db, err := setupDatabase()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to setup database: %v", err)
|
||||
}
|
||||
|
||||
// Template queries
|
||||
fmt.Println("=== TEMPLATE QUERY EXAMPLES ===")
|
||||
templateExamples(db)
|
||||
|
||||
// Channel streaming
|
||||
fmt.Println("\n=== CHANNEL STREAMING EXAMPLES ===")
|
||||
channelExamples(db)
|
||||
|
||||
// Function receivers
|
||||
fmt.Println("\n=== FUNCTION RECEIVER EXAMPLES ===")
|
||||
functionReceiverExamples(db)
|
||||
|
||||
// JSON handling
|
||||
fmt.Println("\n=== JSON HANDLING EXAMPLES ===")
|
||||
jsonExamples(db)
|
||||
|
||||
// Raw SQL
|
||||
fmt.Println("\n=== RAW SQL EXAMPLES ===")
|
||||
rawSQLExamples(db)
|
||||
|
||||
// Complex queries
|
||||
fmt.Println("\n=== COMPLEX QUERY EXAMPLES ===")
|
||||
complexQueryExamples(db)
|
||||
}
|
||||
|
||||
// templateExamples demonstrates Go template syntax in queries
|
||||
func templateExamples(db *mysql.Database) {
|
||||
// Example 1: Conditional WHERE clause
|
||||
fmt.Println("1. Conditional WHERE clause")
|
||||
|
||||
type SearchParams struct {
|
||||
MinAge int
|
||||
Status string
|
||||
Name string
|
||||
}
|
||||
|
||||
// Search with all parameters
|
||||
params := SearchParams{
|
||||
MinAge: 25,
|
||||
Status: "active",
|
||||
Name: "Alice",
|
||||
}
|
||||
|
||||
query := "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`" +
|
||||
" WHERE 1=1" +
|
||||
" {{ if .MinAge }}AND `age` >= @@MinAge{{ end }}" +
|
||||
" {{ if .Status }}AND `status` = @@Status{{ end }}" +
|
||||
" {{ if .Name }}AND `name` LIKE CONCAT('%', @@Name, '%'){{ end }}"
|
||||
|
||||
var users []User
|
||||
err := db.Select(&users, query, 0, params)
|
||||
if err != nil {
|
||||
log.Printf("Template query failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ Found %d users with filters\n", len(users))
|
||||
}
|
||||
|
||||
// Example 2: Dynamic ORDER BY (with validation)
|
||||
fmt.Println("\n2. Dynamic ORDER BY with whitelisting")
|
||||
|
||||
type SortParams struct {
|
||||
SortBy string
|
||||
SortOrder string
|
||||
}
|
||||
|
||||
// Whitelist allowed columns - identifiers can't be marshaled
|
||||
allowedColumns := map[string]bool{
|
||||
"created_at": true,
|
||||
"name": true,
|
||||
"age": true,
|
||||
}
|
||||
|
||||
sortParams := SortParams{
|
||||
SortBy: "created_at",
|
||||
SortOrder: "DESC",
|
||||
}
|
||||
|
||||
// Validate before using in query
|
||||
if !allowedColumns[sortParams.SortBy] {
|
||||
log.Printf("Invalid sort column: %s", sortParams.SortBy)
|
||||
return
|
||||
}
|
||||
|
||||
sortQuery := "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`" +
|
||||
" WHERE `active` = 1" +
|
||||
" {{ if .SortBy }}" +
|
||||
" ORDER BY {{ .SortBy }} {{ .SortOrder }}" +
|
||||
" {{ end }}"
|
||||
|
||||
err = db.Select(&users, sortQuery, 0, sortParams)
|
||||
if err != nil {
|
||||
log.Printf("Sort query failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ Users sorted by %s %s\n", sortParams.SortBy, sortParams.SortOrder)
|
||||
}
|
||||
|
||||
// Example 3: Conditional JOINs
|
||||
fmt.Println("\n3. Conditional JOINs")
|
||||
|
||||
type JoinParams struct {
|
||||
IncludeOrders bool
|
||||
IncludeAddress bool
|
||||
IncludeMetadata bool
|
||||
}
|
||||
|
||||
joinParams := JoinParams{
|
||||
IncludeOrders: true,
|
||||
IncludeAddress: false,
|
||||
}
|
||||
|
||||
joinQuery := "SELECT `users`.`id`, `users`.`name`, `users`.`email`, `users`.`age`, `users`.`active`, `users`.`created_at`, `users`.`updated_at`" +
|
||||
" {{ if .IncludeOrders }}, COUNT(`orders`.`id`) as `order_count`{{ end }}" +
|
||||
" {{ if .IncludeAddress }}, `addresses`.`city`{{ end }}" +
|
||||
" FROM `users`" +
|
||||
" {{ if .IncludeOrders }}" +
|
||||
" LEFT JOIN `orders` ON `users`.`id` = `orders`.`user_id`" +
|
||||
" {{ end }}" +
|
||||
" {{ if .IncludeAddress }}" +
|
||||
" LEFT JOIN `addresses` ON `users`.`id` = `addresses`.`user_id`" +
|
||||
" {{ end }}" +
|
||||
" GROUP BY `users`.`id`"
|
||||
|
||||
err = db.Select(&users, joinQuery, 0, joinParams)
|
||||
if err != nil {
|
||||
log.Printf("Join query failed: %v", err)
|
||||
} else {
|
||||
fmt.Println("✓ Query with conditional joins executed")
|
||||
}
|
||||
|
||||
// Example 4: Custom template functions
|
||||
fmt.Println("\n4. Custom template functions")
|
||||
|
||||
// Add custom functions
|
||||
db.AddTemplateFuncs(template.FuncMap{
|
||||
"upper": strings.ToUpper,
|
||||
"lower": strings.ToLower,
|
||||
"quote": func(s string) string { return fmt.Sprintf("'%s'", s) },
|
||||
})
|
||||
|
||||
type CaseParams struct {
|
||||
SearchTerm string
|
||||
CaseSensitive bool
|
||||
UseWildcard bool
|
||||
}
|
||||
|
||||
caseParams := CaseParams{
|
||||
SearchTerm: "alice",
|
||||
CaseSensitive: false,
|
||||
UseWildcard: true,
|
||||
}
|
||||
|
||||
// IMPORTANT: Template values must be marshaled with | marshal
|
||||
caseQuery := "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`" +
|
||||
" WHERE {{ if not .CaseSensitive }}UPPER(`name`){{ else }}`name`{{ end }} LIKE CONCAT('%', {{ .SearchTerm | marshal }}, '%')"
|
||||
|
||||
err = db.Select(&users, caseQuery, 0, caseParams)
|
||||
if err != nil {
|
||||
log.Printf("Custom function query failed: %v", err)
|
||||
} else {
|
||||
fmt.Println("✓ Query with custom template functions executed")
|
||||
}
|
||||
|
||||
// Example 5: Complex conditional logic
|
||||
fmt.Println("\n5. Complex conditional logic")
|
||||
|
||||
type FilterParams struct {
|
||||
AgeRange []int
|
||||
Statuses []string
|
||||
DateFrom time.Time
|
||||
DateTo time.Time
|
||||
ActiveOnly bool
|
||||
}
|
||||
|
||||
filterParams := FilterParams{
|
||||
AgeRange: []int{25, 40},
|
||||
ActiveOnly: true,
|
||||
DateFrom: time.Now().Add(-30 * 24 * time.Hour),
|
||||
}
|
||||
|
||||
filterQuery := "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`" +
|
||||
" WHERE 1=1" +
|
||||
" {{ if .AgeRange }}" +
|
||||
" AND `age` BETWEEN @@AgeMin AND @@AgeMax" +
|
||||
" {{ end }}" +
|
||||
" {{ if .ActiveOnly }}" +
|
||||
" AND `active` = 1" +
|
||||
" {{ end }}" +
|
||||
" {{ if not .DateFrom.IsZero }}" +
|
||||
" AND `created_at` >= @@DateFrom" +
|
||||
" {{ end }}" +
|
||||
" {{ if not .DateTo.IsZero }}" +
|
||||
" AND `created_at` <= @@DateTo" +
|
||||
" {{ end }}"
|
||||
|
||||
queryParams := mysql.Params{
|
||||
"AgeMin": filterParams.AgeRange[0],
|
||||
"AgeMax": filterParams.AgeRange[1],
|
||||
"DateFrom": filterParams.DateFrom,
|
||||
}
|
||||
|
||||
err = db.Select(&users, filterQuery, 0, queryParams, filterParams)
|
||||
if err != nil {
|
||||
log.Printf("Complex filter query failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ Found %d users with complex filters\n", len(users))
|
||||
}
|
||||
}
|
||||
|
||||
// channelExamples demonstrates streaming with channels
|
||||
func channelExamples(db *mysql.Database) {
|
||||
// Example 1: Stream SELECT results
|
||||
fmt.Println("1. Stream SELECT results to channel")
|
||||
|
||||
userCh := make(chan User, 10) // Buffered channel
|
||||
|
||||
go func() {
|
||||
defer close(userCh)
|
||||
err := db.Select(userCh, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `active` = 1", 0)
|
||||
if err != nil {
|
||||
log.Printf("Channel select failed: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
count := 0
|
||||
for user := range userCh {
|
||||
fmt.Printf(" Received: %s (%s)\n", user.Name, user.Email)
|
||||
count++
|
||||
if count >= 5 {
|
||||
fmt.Println(" (showing first 5 results)")
|
||||
// Drain remaining
|
||||
for range userCh {
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Example 2: Stream INSERT from channel
|
||||
fmt.Println("\n2. Stream INSERT from channel")
|
||||
|
||||
insertCh := make(chan User, 10)
|
||||
|
||||
go func() {
|
||||
defer close(insertCh)
|
||||
for i := 0; i < 100; i++ {
|
||||
insertCh <- User{
|
||||
Name: fmt.Sprintf("StreamUser%d", i),
|
||||
Email: fmt.Sprintf("stream%d@example.com", i),
|
||||
Age: 20 + (i % 50),
|
||||
Active: i%2 == 0,
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err := db.Insert("users", insertCh)
|
||||
if err != nil {
|
||||
log.Printf("Channel insert failed: %v", err)
|
||||
} else {
|
||||
fmt.Println("✓ Streamed 100 users for insertion")
|
||||
}
|
||||
|
||||
// Example 3: Transform while streaming
|
||||
fmt.Println("\n3. Transform data while streaming")
|
||||
|
||||
type EnrichedUser struct {
|
||||
User
|
||||
Category string
|
||||
Priority int
|
||||
}
|
||||
|
||||
rawUserCh := make(chan User, 10)
|
||||
enrichedCh := make(chan EnrichedUser, 10)
|
||||
|
||||
// Producer: fetch users
|
||||
go func() {
|
||||
defer close(rawUserCh)
|
||||
db.Select(rawUserCh, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` LIMIT @@limit", 0, 50)
|
||||
}()
|
||||
|
||||
// Transformer: enrich data
|
||||
go func() {
|
||||
defer close(enrichedCh)
|
||||
for user := range rawUserCh {
|
||||
enriched := EnrichedUser{
|
||||
User: user,
|
||||
}
|
||||
|
||||
// Add category based on age
|
||||
if user.Age < 25 {
|
||||
enriched.Category = "Young"
|
||||
enriched.Priority = 1
|
||||
} else if user.Age < 40 {
|
||||
enriched.Category = "Middle"
|
||||
enriched.Priority = 2
|
||||
} else {
|
||||
enriched.Category = "Senior"
|
||||
enriched.Priority = 3
|
||||
}
|
||||
|
||||
enrichedCh <- enriched
|
||||
}
|
||||
}()
|
||||
|
||||
// Consumer: process enriched data
|
||||
processed := 0
|
||||
for enriched := range enrichedCh {
|
||||
processed++
|
||||
_ = enriched // Process enriched user
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Processed %d enriched users\n", processed)
|
||||
}
|
||||
|
||||
// functionReceiverExamples demonstrates function receivers
|
||||
func functionReceiverExamples(db *mysql.Database) {
|
||||
// Example 1: Process each row with function
|
||||
fmt.Println("1. Process rows with function")
|
||||
|
||||
count := 0
|
||||
err := db.Select(func(u User) {
|
||||
count++
|
||||
if count <= 3 {
|
||||
fmt.Printf(" Processing: %s (Age: %d)\n", u.Name, u.Age)
|
||||
}
|
||||
}, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `active` = 1", 0)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Function receiver failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ Processed %d users\n", count)
|
||||
}
|
||||
|
||||
// Example 2: Aggregate data with function
|
||||
fmt.Println("\n2. Aggregate data with function")
|
||||
|
||||
var totalAge int
|
||||
var userCount int
|
||||
|
||||
err = db.Select(func(u User) {
|
||||
totalAge += u.Age
|
||||
userCount++
|
||||
}, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Aggregation failed: %v", err)
|
||||
} else if userCount > 0 {
|
||||
avgAge := float64(totalAge) / float64(userCount)
|
||||
fmt.Printf("✓ Average age: %.2f (%d users)\n", avgAge, userCount)
|
||||
}
|
||||
|
||||
// Example 3: Conditional processing
|
||||
fmt.Println("\n3. Conditional processing with function")
|
||||
|
||||
type Stats struct {
|
||||
YoungCount int
|
||||
MiddleCount int
|
||||
SeniorCount int
|
||||
}
|
||||
|
||||
stats := Stats{}
|
||||
|
||||
err = db.Select(func(u User) {
|
||||
switch {
|
||||
case u.Age < 25:
|
||||
stats.YoungCount++
|
||||
case u.Age < 40:
|
||||
stats.MiddleCount++
|
||||
default:
|
||||
stats.SeniorCount++
|
||||
}
|
||||
}, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Stats failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ Age distribution: Young=%d, Middle=%d, Senior=%d\n",
|
||||
stats.YoungCount, stats.MiddleCount, stats.SeniorCount)
|
||||
}
|
||||
|
||||
// Example 4: Early termination pattern
|
||||
fmt.Println("\n4. Early termination with function")
|
||||
|
||||
found := false
|
||||
targetEmail := "alice@example.com"
|
||||
|
||||
err = db.Select(func(u User) {
|
||||
if u.Email == targetEmail {
|
||||
found = true
|
||||
fmt.Printf("✓ Found user: %s\n", u.Name)
|
||||
// Note: Can't actually stop iteration early
|
||||
// This is a limitation of function receivers
|
||||
}
|
||||
}, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Search failed: %v", err)
|
||||
} else if !found {
|
||||
fmt.Println("✗ User not found")
|
||||
}
|
||||
}
|
||||
|
||||
// jsonExamples demonstrates JSON handling
|
||||
func jsonExamples(db *mysql.Database) {
|
||||
// Example 1: Store JSON in struct field
|
||||
fmt.Println("1. Store JSON column in struct")
|
||||
|
||||
type UserWithMeta struct {
|
||||
ID int `mysql:"id"`
|
||||
Name string `mysql:"name"`
|
||||
Email string `mysql:"email"`
|
||||
Metadata json.RawMessage `mysql:"metadata"` // JSON column
|
||||
}
|
||||
|
||||
userMeta := UserWithMeta{
|
||||
Name: "JSONUser",
|
||||
Email: "json@example.com",
|
||||
Metadata: json.RawMessage(`{
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"notifications": true
|
||||
}`),
|
||||
}
|
||||
|
||||
err := db.Insert("users", userMeta)
|
||||
if err != nil {
|
||||
log.Printf("JSON insert failed: %v", err)
|
||||
} else {
|
||||
fmt.Println("✓ User with JSON metadata inserted")
|
||||
}
|
||||
|
||||
// Example 2: Select JSON as RawMessage
|
||||
fmt.Println("\n2. Select JSON column")
|
||||
|
||||
var usersWithMeta []UserWithMeta
|
||||
err = db.Select(&usersWithMeta,
|
||||
"SELECT `id`, `name`, `email`, metadata FROM `users` WHERE metadata IS NOT NULL LIMIT @@limit",
|
||||
0,
|
||||
5)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("JSON select failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ Retrieved %d users with metadata\n", len(usersWithMeta))
|
||||
for _, u := range usersWithMeta {
|
||||
fmt.Printf(" %s: %s\n", u.Name, string(u.Metadata))
|
||||
}
|
||||
}
|
||||
|
||||
// Example 3: SelectJSON for JSON result
|
||||
fmt.Println("\n3. SelectJSON for JSON object")
|
||||
|
||||
var jsonResult json.RawMessage
|
||||
err = db.SelectJSON(&jsonResult,
|
||||
"SELECT JSON_OBJECT("+
|
||||
" 'id', `id`,"+
|
||||
" 'name', `name`,"+
|
||||
" 'email', `email`,"+
|
||||
" 'age', `age`"+
|
||||
" ) FROM `users` WHERE `id` = @@id",
|
||||
0,
|
||||
1)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("SelectJSON failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ JSON result: %s\n", string(jsonResult))
|
||||
}
|
||||
|
||||
// Example 4: SelectJSON for JSON array
|
||||
fmt.Println("\n4. SelectJSON for JSON array")
|
||||
|
||||
var jsonArray json.RawMessage
|
||||
err = db.SelectJSON(&jsonArray,
|
||||
"SELECT JSON_ARRAYAGG("+
|
||||
" JSON_OBJECT("+
|
||||
" 'name', `name`,"+
|
||||
" 'email', `email`"+
|
||||
" )"+
|
||||
" ) FROM `users` WHERE `active` = 1 LIMIT @@limit",
|
||||
0,
|
||||
5)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("SelectJSON array failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ JSON array result: %s\n", string(jsonArray))
|
||||
}
|
||||
}
|
||||
|
||||
// rawSQLExamples demonstrates using mysql.Raw for literal SQL
|
||||
func rawSQLExamples(db *mysql.Database) {
|
||||
// Example 1: Raw SQL in WHERE clause
|
||||
fmt.Println("1. Raw SQL for complex condition")
|
||||
|
||||
var users []User
|
||||
err := db.Select(&users,
|
||||
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE @@condition",
|
||||
0,
|
||||
mysql.Raw("created_at > NOW() - INTERVAL 7 DAY"))
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Raw SQL query failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ Found %d users from last 7 days\n", len(users))
|
||||
}
|
||||
|
||||
// Example 2: Raw SQL for CASE statement
|
||||
fmt.Println("\n2. Raw SQL for CASE statement")
|
||||
|
||||
type UserWithLabel struct {
|
||||
Name string `mysql:"name"`
|
||||
Label string `mysql:"label"`
|
||||
}
|
||||
|
||||
caseSQL := mysql.Raw(`
|
||||
CASE
|
||||
WHEN age < 25 THEN 'Young'
|
||||
WHEN age < 40 THEN 'Middle'
|
||||
ELSE 'Senior'
|
||||
END
|
||||
`)
|
||||
|
||||
var labeled []UserWithLabel
|
||||
err = db.Select(&labeled,
|
||||
"SELECT name, @@ageCase as `label` FROM `users`",
|
||||
0,
|
||||
caseSQL)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("CASE query failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ Retrieved %d users with age labels\n", len(labeled))
|
||||
for i, u := range labeled {
|
||||
if i < 3 {
|
||||
fmt.Printf(" %s: %s\n", u.Name, u.Label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example 3: Raw SQL for subquery
|
||||
fmt.Println("\n3. Raw SQL for subquery")
|
||||
|
||||
subquery := mysql.Raw("(SELECT AVG(age) FROM `users` WHERE `active` = 1)")
|
||||
|
||||
err = db.Select(&users,
|
||||
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@avgAge",
|
||||
0,
|
||||
subquery)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Subquery failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ Found %d users above average age\n", len(users))
|
||||
}
|
||||
|
||||
// Example 4: WARNING - Never use Raw with user input!
|
||||
fmt.Println("\n4. WARNING: Raw SQL security example")
|
||||
|
||||
// DANGEROUS - SQL injection risk!
|
||||
// userInput := "'; DROP TABLE users; --"
|
||||
// db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `name` = @@name", 0,
|
||||
// mysql.Params{"name": mysql.Raw(userInput)})
|
||||
|
||||
// SAFE - use regular parameter
|
||||
safeInput := "Alice'; DROP TABLE users; --"
|
||||
err = db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `name` = @@name", 0, safeInput) // Properly escaped
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Safe query failed: %v", err)
|
||||
} else {
|
||||
fmt.Println("✓ User input safely escaped (no SQL injection)")
|
||||
}
|
||||
}
|
||||
|
||||
// complexQueryExamples demonstrates complex query patterns
|
||||
func complexQueryExamples(db *mysql.Database) {
|
||||
// Example 1: Subquery with named parameters
|
||||
fmt.Println("1. Subquery with parameters")
|
||||
|
||||
type UserWithOrderCount struct {
|
||||
User
|
||||
OrderCount int `mysql:"order_count"`
|
||||
}
|
||||
|
||||
var usersWithOrders []UserWithOrderCount
|
||||
err := db.Select(&usersWithOrders,
|
||||
"SELECT `users`.`id`, `users`.`name`, `users`.`email`, `users`.`age`, `users`.`active`, `users`.`created_at`, `users`.`updated_at`,"+
|
||||
" (SELECT COUNT(*) FROM `orders` WHERE `orders`.`user_id` = `users`.`id`) as `order_count`"+
|
||||
" FROM `users`"+
|
||||
" WHERE `users`.`created_at` > @@since"+
|
||||
" AND `users`.`active` = @@active",
|
||||
5*time.Minute,
|
||||
mysql.Params{
|
||||
"since": time.Now().Add(-30 * 24 * time.Hour),
|
||||
"active": true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Subquery failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ Retrieved %d active users with order counts\n", len(usersWithOrders))
|
||||
}
|
||||
|
||||
// Example 2: JOIN with aggregation
|
||||
fmt.Println("\n2. JOIN with aggregation")
|
||||
|
||||
query := "SELECT" +
|
||||
" `users`.`id`," +
|
||||
" `users`.`name`," +
|
||||
" `users`.`email`," +
|
||||
" COUNT(`orders`.`id`) as `order_count`," +
|
||||
" SUM(`orders`.`total`) as `total_spent`" +
|
||||
" FROM `users`" +
|
||||
" LEFT JOIN `orders` ON `users`.`id` = `orders`.`user_id`" +
|
||||
" WHERE `users`.`active` = @@active" +
|
||||
" GROUP BY `users`.`id`, `users`.`name`, `users`.`email`" +
|
||||
" HAVING COUNT(`orders`.`id`) > @@minOrders" +
|
||||
" ORDER BY total_spent DESC" +
|
||||
" LIMIT @@limit"
|
||||
|
||||
type UserStats struct {
|
||||
ID int `mysql:"id"`
|
||||
Name string `mysql:"name"`
|
||||
Email string `mysql:"email"`
|
||||
OrderCount int `mysql:"order_count"`
|
||||
TotalSpent float64 `mysql:"total_spent"`
|
||||
}
|
||||
|
||||
var stats []UserStats
|
||||
err = db.Select(&stats, query, 10*time.Minute,
|
||||
mysql.Params{
|
||||
"active": true,
|
||||
"minOrders": 5,
|
||||
"limit": 10,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Aggregation query failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ Top %d spenders retrieved\n", len(stats))
|
||||
}
|
||||
|
||||
// Example 3: Window function
|
||||
fmt.Println("\n3. Window function query")
|
||||
|
||||
windowQuery := "SELECT" +
|
||||
" `id`," +
|
||||
" `name`," +
|
||||
" `age`," +
|
||||
" RANK() OVER (ORDER BY `age` DESC) as `age_rank`," +
|
||||
" AVG(`age`) OVER () as `avg_age`" +
|
||||
" FROM `users`" +
|
||||
" WHERE `active` = @@active" +
|
||||
" LIMIT @@limit"
|
||||
|
||||
type UserWithRank struct {
|
||||
ID int `mysql:"id"`
|
||||
Name string `mysql:"name"`
|
||||
Age int `mysql:"age"`
|
||||
AgeRank int `mysql:"age_rank"`
|
||||
AvgAge float64 `mysql:"avg_age"`
|
||||
}
|
||||
|
||||
var ranked []UserWithRank
|
||||
err = db.Select(&ranked, windowQuery, 5*time.Minute,
|
||||
mysql.Params{
|
||||
"active": true,
|
||||
"limit": 20,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Window function query failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ Retrieved %d users with age ranking\n", len(ranked))
|
||||
}
|
||||
|
||||
// Example 4: CTE (Common Table Expression)
|
||||
fmt.Println("\n4. CTE query")
|
||||
|
||||
cteQuery := "WITH recent_users AS (" +
|
||||
" SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`" +
|
||||
" WHERE `created_at` > @@since" +
|
||||
" )," +
|
||||
" active_recent AS (" +
|
||||
" SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM recent_users" +
|
||||
" WHERE `active` = @@active" +
|
||||
" )" +
|
||||
" SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM active_recent" +
|
||||
" ORDER BY `created_at` DESC" +
|
||||
" LIMIT @@limit"
|
||||
|
||||
var cteUsers []User
|
||||
err = db.Select(&cteUsers, cteQuery, 5*time.Minute,
|
||||
mysql.Params{
|
||||
"since": time.Now().Add(-7 * 24 * time.Hour),
|
||||
"active": true,
|
||||
"limit": 10,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Printf("CTE query failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ Retrieved %d recent active users via CTE\n", len(cteUsers))
|
||||
}
|
||||
}
|
||||
517
examples/basic-crud.go
Normal file
517
examples/basic-crud.go
Normal file
@@ -0,0 +1,517 @@
|
||||
// Package examples demonstrates basic CRUD operations with cool-mysql
|
||||
package examples
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
mysql "github.com/StirlingMarketingGroup/cool-mysql"
|
||||
)
|
||||
|
||||
// User represents a user in the database
|
||||
type User struct {
|
||||
ID int `mysql:"id"`
|
||||
Name string `mysql:"name"`
|
||||
Email string `mysql:"email"`
|
||||
Age int `mysql:"age"`
|
||||
Active bool `mysql:"active"`
|
||||
CreatedAt time.Time `mysql:"created_at,defaultzero"`
|
||||
UpdatedAt time.Time `mysql:"updated_at,defaultzero"`
|
||||
}
|
||||
|
||||
// BasicCRUDExamples demonstrates basic Create, Read, Update, Delete operations
|
||||
func BasicCRUDExamples() {
|
||||
// Setup database connection
|
||||
db, err := setupDatabase()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to setup database: %v", err)
|
||||
}
|
||||
|
||||
// Create
|
||||
fmt.Println("=== CREATE EXAMPLES ===")
|
||||
createExamples(db)
|
||||
|
||||
// Read
|
||||
fmt.Println("\n=== READ EXAMPLES ===")
|
||||
readExamples(db)
|
||||
|
||||
// Update
|
||||
fmt.Println("\n=== UPDATE EXAMPLES ===")
|
||||
updateExamples(db)
|
||||
|
||||
// Delete
|
||||
fmt.Println("\n=== DELETE EXAMPLES ===")
|
||||
deleteExamples(db)
|
||||
|
||||
// Utility queries
|
||||
fmt.Println("\n=== UTILITY EXAMPLES ===")
|
||||
utilityExamples(db)
|
||||
}
|
||||
|
||||
// setupDatabase creates a connection to MySQL
|
||||
func setupDatabase() (*mysql.Database, error) {
|
||||
// Create database connection with read/write pools
|
||||
db, err := mysql.New(
|
||||
"root", // write user
|
||||
"password", // write password
|
||||
"mydb", // write schema
|
||||
"localhost", // write host
|
||||
3306, // write port
|
||||
"root", // read user
|
||||
"password", // read password
|
||||
"mydb", // read schema
|
||||
"localhost", // read host
|
||||
3306, // read port
|
||||
"utf8mb4_unicode_ci", // collation
|
||||
time.UTC, // timezone
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create database: %w", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// createExamples demonstrates INSERT operations
|
||||
func createExamples(db *mysql.Database) {
|
||||
// Example 1: Insert single user
|
||||
fmt.Println("1. Insert single user")
|
||||
user := User{
|
||||
Name: "Alice",
|
||||
Email: "alice@example.com",
|
||||
Age: 25,
|
||||
Active: true,
|
||||
}
|
||||
|
||||
err := db.Insert("users", user)
|
||||
if err != nil {
|
||||
log.Printf("Insert failed: %v", err)
|
||||
} else {
|
||||
fmt.Println("✓ User inserted successfully")
|
||||
}
|
||||
|
||||
// Example 2: Insert with explicit zero values
|
||||
fmt.Println("\n2. Insert with timestamp defaults")
|
||||
userWithDefaults := User{
|
||||
Name: "Bob",
|
||||
Email: "bob@example.com",
|
||||
Age: 30,
|
||||
Active: true,
|
||||
// CreatedAt and UpdatedAt are zero values
|
||||
// With ,defaultzero tag, database will use DEFAULT values
|
||||
}
|
||||
|
||||
err = db.Insert("users", userWithDefaults)
|
||||
if err != nil {
|
||||
log.Printf("Insert failed: %v", err)
|
||||
} else {
|
||||
fmt.Println("✓ User inserted with database defaults")
|
||||
}
|
||||
|
||||
// Example 3: Batch insert
|
||||
fmt.Println("\n3. Batch insert multiple users")
|
||||
users := []User{
|
||||
{Name: "Charlie", Email: "charlie@example.com", Age: 28, Active: true},
|
||||
{Name: "Diana", Email: "diana@example.com", Age: 32, Active: true},
|
||||
{Name: "Eve", Email: "eve@example.com", Age: 27, Active: false},
|
||||
}
|
||||
|
||||
err = db.Insert("users", users)
|
||||
if err != nil {
|
||||
log.Printf("Batch insert failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ Batch inserted %d users\n", len(users))
|
||||
}
|
||||
|
||||
// Example 4: Insert with context
|
||||
fmt.Println("\n4. Insert with context timeout")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
contextUser := User{
|
||||
Name: "Frank",
|
||||
Email: "frank@example.com",
|
||||
Age: 35,
|
||||
Active: true,
|
||||
}
|
||||
|
||||
err = db.InsertContext(ctx, "users", contextUser)
|
||||
if err != nil {
|
||||
log.Printf("Insert with context failed: %v", err)
|
||||
} else {
|
||||
fmt.Println("✓ User inserted with context")
|
||||
}
|
||||
}
|
||||
|
||||
// readExamples demonstrates SELECT operations
|
||||
func readExamples(db *mysql.Database) {
|
||||
// Example 1: Select all users into slice
|
||||
fmt.Println("1. Select all users")
|
||||
var allUsers []User
|
||||
err := db.Select(&allUsers, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
|
||||
if err != nil {
|
||||
log.Printf("Select all failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ Found %d users\n", len(allUsers))
|
||||
for _, u := range allUsers {
|
||||
fmt.Printf(" - %s (%s), Age: %d\n", u.Name, u.Email, u.Age)
|
||||
}
|
||||
}
|
||||
|
||||
// Example 2: Select with named parameters
|
||||
fmt.Println("\n2. Select users with age filter")
|
||||
var adults []User
|
||||
err = db.Select(&adults,
|
||||
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age >= @@minAge",
|
||||
0, // No caching
|
||||
25)
|
||||
if err != nil {
|
||||
log.Printf("Select with filter failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ Found %d users aged 25+\n", len(adults))
|
||||
}
|
||||
|
||||
// Example 3: Select single user
|
||||
fmt.Println("\n3. Select single user by email")
|
||||
var user User
|
||||
err = db.Select(&user,
|
||||
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `email` = @@email",
|
||||
0,
|
||||
"alice@example.com")
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
fmt.Println("✗ User not found")
|
||||
} else if err != nil {
|
||||
log.Printf("Select failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ Found user: %s (ID: %d)\n", user.Name, user.ID)
|
||||
}
|
||||
|
||||
// Example 4: Select single value
|
||||
fmt.Println("\n4. Select single value (name)")
|
||||
var name string
|
||||
err = db.Select(&name,
|
||||
"SELECT `name` FROM `users` WHERE `email` = @@email",
|
||||
0,
|
||||
"bob@example.com")
|
||||
if err != nil {
|
||||
log.Printf("Select name failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ User name: %s\n", name)
|
||||
}
|
||||
|
||||
// Example 5: Select with multiple conditions
|
||||
fmt.Println("\n5. Select with multiple conditions")
|
||||
var activeAdults []User
|
||||
err = db.Select(&activeAdults,
|
||||
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`"+
|
||||
" WHERE `age` >= @@minAge"+
|
||||
" AND `active` = @@active"+
|
||||
" ORDER BY `name`",
|
||||
0,
|
||||
mysql.Params{
|
||||
"minAge": 25,
|
||||
"active": true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Select failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ Found %d active adult users\n", len(activeAdults))
|
||||
}
|
||||
|
||||
// Example 6: Select with caching
|
||||
fmt.Println("\n6. Select with caching (5 minute TTL)")
|
||||
var cachedUsers []User
|
||||
err = db.Select(&cachedUsers,
|
||||
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `active` = @@active",
|
||||
5*time.Minute, // Cache for 5 minutes
|
||||
true)
|
||||
if err != nil {
|
||||
log.Printf("Cached select failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ Found %d active users (cached)\n", len(cachedUsers))
|
||||
}
|
||||
|
||||
// Example 7: Select with context
|
||||
fmt.Println("\n7. Select with context timeout")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var contextUsers []User
|
||||
err = db.SelectContext(ctx, &contextUsers,
|
||||
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` LIMIT @@limit",
|
||||
0,
|
||||
10)
|
||||
if err != nil {
|
||||
log.Printf("Select with context failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ Found %d users with context\n", len(contextUsers))
|
||||
}
|
||||
}
|
||||
|
||||
// updateExamples demonstrates UPDATE operations
|
||||
func updateExamples(db *mysql.Database) {
|
||||
// Example 1: Simple update
|
||||
fmt.Println("1. Update user name")
|
||||
err := db.Exec(
|
||||
"UPDATE `users` SET `name` = @@name WHERE `email` = @@email",
|
||||
mysql.Params{
|
||||
"name": "Alice Smith",
|
||||
"email": "alice@example.com",
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Update failed: %v", err)
|
||||
} else {
|
||||
fmt.Println("✓ User name updated")
|
||||
}
|
||||
|
||||
// Example 2: Update with result
|
||||
fmt.Println("\n2. Update with result check")
|
||||
result, err := db.ExecResult(
|
||||
"UPDATE `users` SET `age` = @@age WHERE `email` = @@email",
|
||||
mysql.Params{
|
||||
"age": 26,
|
||||
"email": "alice@example.com",
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Update failed: %v", err)
|
||||
} else {
|
||||
rowsAffected, _ := result.RowsAffected()
|
||||
fmt.Printf("✓ Updated %d row(s)\n", rowsAffected)
|
||||
}
|
||||
|
||||
// Example 3: Update multiple rows
|
||||
fmt.Println("\n3. Update multiple rows")
|
||||
result, err = db.ExecResult(
|
||||
"UPDATE `users` SET `active` = @@active WHERE age < @@maxAge",
|
||||
mysql.Params{
|
||||
"active": false,
|
||||
"maxAge": 25,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Bulk update failed: %v", err)
|
||||
} else {
|
||||
rowsAffected, _ := result.RowsAffected()
|
||||
fmt.Printf("✓ Deactivated %d user(s)\n", rowsAffected)
|
||||
}
|
||||
|
||||
// Example 4: Update with current timestamp
|
||||
fmt.Println("\n4. Update timestamp")
|
||||
err = db.Exec(
|
||||
"UPDATE `users` SET `updated_at` = NOW() WHERE `email` = @@email",
|
||||
"bob@example.com")
|
||||
if err != nil {
|
||||
log.Printf("Update timestamp failed: %v", err)
|
||||
} else {
|
||||
fmt.Println("✓ Timestamp updated")
|
||||
}
|
||||
|
||||
// Example 5: Conditional update
|
||||
fmt.Println("\n5. Conditional update (only if age is current value)")
|
||||
err = db.Exec(
|
||||
"UPDATE `users`"+
|
||||
" SET `age` = @@newAge"+
|
||||
" WHERE `email` = @@email"+
|
||||
" AND `age` = @@currentAge",
|
||||
mysql.Params{
|
||||
"newAge": 27,
|
||||
"email": "charlie@example.com",
|
||||
"currentAge": 28,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Conditional update failed: %v", err)
|
||||
} else {
|
||||
fmt.Println("✓ Conditional update executed")
|
||||
}
|
||||
|
||||
// Example 6: Update with context
|
||||
fmt.Println("\n6. Update with context")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err = db.ExecContext(ctx,
|
||||
"UPDATE `users` SET `active` = @@active WHERE age > @@age",
|
||||
mysql.Params{
|
||||
"active": true,
|
||||
"age": 30,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Update with context failed: %v", err)
|
||||
} else {
|
||||
fmt.Println("✓ Update executed with context")
|
||||
}
|
||||
}
|
||||
|
||||
// deleteExamples demonstrates DELETE operations
|
||||
func deleteExamples(db *mysql.Database) {
|
||||
// Example 1: Delete single record
|
||||
fmt.Println("1. Delete single user")
|
||||
err := db.Exec(
|
||||
"DELETE FROM `users` WHERE `email` = @@email",
|
||||
"eve@example.com")
|
||||
if err != nil {
|
||||
log.Printf("Delete failed: %v", err)
|
||||
} else {
|
||||
fmt.Println("✓ User deleted")
|
||||
}
|
||||
|
||||
// Example 2: Delete with result check
|
||||
fmt.Println("\n2. Delete with result check")
|
||||
result, err := db.ExecResult(
|
||||
"DELETE FROM `users` WHERE `email` = @@email",
|
||||
"frank@example.com")
|
||||
if err != nil {
|
||||
log.Printf("Delete failed: %v", err)
|
||||
} else {
|
||||
rowsAffected, _ := result.RowsAffected()
|
||||
if rowsAffected == 0 {
|
||||
fmt.Println("✗ No user found to delete")
|
||||
} else {
|
||||
fmt.Printf("✓ Deleted %d user(s)\n", rowsAffected)
|
||||
}
|
||||
}
|
||||
|
||||
// Example 3: Delete multiple records
|
||||
fmt.Println("\n3. Delete inactive users")
|
||||
result, err = db.ExecResult(
|
||||
"DELETE FROM `users` WHERE `active` = @@active",
|
||||
false)
|
||||
if err != nil {
|
||||
log.Printf("Bulk delete failed: %v", err)
|
||||
} else {
|
||||
rowsAffected, _ := result.RowsAffected()
|
||||
fmt.Printf("✓ Deleted %d inactive user(s)\n", rowsAffected)
|
||||
}
|
||||
|
||||
// Example 4: Delete with age condition
|
||||
fmt.Println("\n4. Delete users under age threshold")
|
||||
result, err = db.ExecResult(
|
||||
"DELETE FROM `users` WHERE age < @@minAge",
|
||||
18)
|
||||
if err != nil {
|
||||
log.Printf("Delete failed: %v", err)
|
||||
} else {
|
||||
rowsAffected, _ := result.RowsAffected()
|
||||
fmt.Printf("✓ Deleted %d user(s) under 18\n", rowsAffected)
|
||||
}
|
||||
|
||||
// Example 5: Delete with context
|
||||
fmt.Println("\n5. Delete with context")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err = db.ExecContext(ctx,
|
||||
"DELETE FROM `users` WHERE `created_at` < @@cutoff",
|
||||
time.Now().Add(-365*24*time.Hour))
|
||||
if err != nil {
|
||||
log.Printf("Delete with context failed: %v", err)
|
||||
} else {
|
||||
fmt.Println("✓ Old users deleted with context")
|
||||
}
|
||||
}
|
||||
|
||||
// utilityExamples demonstrates utility query methods
|
||||
func utilityExamples(db *mysql.Database) {
|
||||
// Example 1: Count users
|
||||
fmt.Println("1. Count all users")
|
||||
count, err := db.Count(
|
||||
"SELECT COUNT(*) FROM `users`",
|
||||
0) // No caching
|
||||
if err != nil {
|
||||
log.Printf("Count failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ Total users: %d\n", count)
|
||||
}
|
||||
|
||||
// Example 2: Count with condition
|
||||
fmt.Println("\n2. Count active users")
|
||||
activeCount, err := db.Count(
|
||||
"SELECT COUNT(*) FROM `users` WHERE `active` = @@active",
|
||||
0,
|
||||
true)
|
||||
if err != nil {
|
||||
log.Printf("Count failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ Active users: %d\n", activeCount)
|
||||
}
|
||||
|
||||
// Example 3: Count with caching
|
||||
fmt.Println("\n3. Count with caching")
|
||||
cachedCount, err := db.Count(
|
||||
"SELECT COUNT(*) FROM `users`",
|
||||
5*time.Minute) // Cache for 5 minutes
|
||||
if err != nil {
|
||||
log.Printf("Cached count failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ Cached user count: %d\n", cachedCount)
|
||||
}
|
||||
|
||||
// Example 4: Check if user exists
|
||||
fmt.Println("\n4. Check if email exists")
|
||||
exists, err := db.Exists(
|
||||
"SELECT 1 FROM `users` WHERE `email` = @@email",
|
||||
0,
|
||||
"alice@example.com")
|
||||
if err != nil {
|
||||
log.Printf("Exists check failed: %v", err)
|
||||
} else {
|
||||
if exists {
|
||||
fmt.Println("✓ Email exists in database")
|
||||
} else {
|
||||
fmt.Println("✗ Email not found")
|
||||
}
|
||||
}
|
||||
|
||||
// Example 5: Check existence with multiple conditions
|
||||
fmt.Println("\n5. Check if active adult exists")
|
||||
exists, err = db.Exists(
|
||||
"SELECT 1 FROM `users`"+
|
||||
" WHERE `active` = @@active"+
|
||||
" AND `age` >= @@minAge",
|
||||
0,
|
||||
mysql.Params{
|
||||
"active": true,
|
||||
"minAge": 25,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Exists check failed: %v", err)
|
||||
} else {
|
||||
if exists {
|
||||
fmt.Println("✓ Active adult user exists")
|
||||
} else {
|
||||
fmt.Println("✗ No active adult users found")
|
||||
}
|
||||
}
|
||||
|
||||
// Example 6: Read-after-write with SelectWrites
|
||||
fmt.Println("\n6. Read-after-write consistency")
|
||||
|
||||
// Insert user
|
||||
newUser := User{
|
||||
Name: "Grace",
|
||||
Email: "grace@example.com",
|
||||
Age: 29,
|
||||
Active: true,
|
||||
}
|
||||
err = db.Insert("users", newUser)
|
||||
if err != nil {
|
||||
log.Printf("Insert failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Immediately read using write pool for consistency
|
||||
var retrieved User
|
||||
err = db.SelectWrites(&retrieved,
|
||||
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `email` = @@email",
|
||||
0, // Don't cache writes
|
||||
"grace@example.com")
|
||||
if err != nil {
|
||||
log.Printf("SelectWrites failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf("✓ User retrieved immediately after insert: %s (ID: %d)\n",
|
||||
retrieved.Name, retrieved.ID)
|
||||
}
|
||||
}
|
||||
534
examples/caching-setup.go
Normal file
534
examples/caching-setup.go
Normal file
@@ -0,0 +1,534 @@
|
||||
// Package examples demonstrates caching configuration with cool-mysql
|
||||
package examples
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/bradfitz/gomemcache/memcache"
|
||||
"github.com/redis/go-redis/v9"
|
||||
mysql "github.com/StirlingMarketingGroup/cool-mysql"
|
||||
)
|
||||
|
||||
// CachingExamples demonstrates various caching setups and strategies
|
||||
func CachingExamples() {
|
||||
fmt.Println("=== CACHING SETUP EXAMPLES ===")
|
||||
|
||||
// In-memory caching
|
||||
fmt.Println("\n1. In-Memory Weak Cache")
|
||||
weakCacheExample()
|
||||
|
||||
// Redis caching
|
||||
fmt.Println("\n2. Redis Cache")
|
||||
redisCacheExample()
|
||||
|
||||
// Redis Cluster caching
|
||||
fmt.Println("\n3. Redis Cluster Cache")
|
||||
redisClusterExample()
|
||||
|
||||
// Memcached caching
|
||||
fmt.Println("\n4. Memcached Cache")
|
||||
memcachedCacheExample()
|
||||
|
||||
// Multi-level caching
|
||||
fmt.Println("\n5. Multi-Level Cache")
|
||||
multiCacheExample()
|
||||
|
||||
// Cache strategies
|
||||
fmt.Println("\n6. Cache Strategies")
|
||||
cacheStrategiesExample()
|
||||
|
||||
// Performance benchmark
|
||||
fmt.Println("\n7. Performance Benchmark")
|
||||
db, err := setupDatabase()
|
||||
if err != nil {
|
||||
log.Printf("Setup failed: %v", err)
|
||||
return
|
||||
}
|
||||
performanceBenchmark(db)
|
||||
|
||||
// Cache key debugging
|
||||
fmt.Println("\n8. Cache Key Debugging")
|
||||
cacheKeyDebug(db)
|
||||
}
|
||||
|
||||
// weakCacheExample demonstrates in-memory weak cache
|
||||
func weakCacheExample() {
|
||||
db, err := setupDatabase()
|
||||
if err != nil {
|
||||
log.Printf("Setup failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Enable in-memory weak cache
|
||||
db.UseCache(mysql.NewWeakCache())
|
||||
|
||||
fmt.Println("✓ Weak cache enabled (GC-managed, local only)")
|
||||
|
||||
// First query - cache miss
|
||||
start := time.Now()
|
||||
var users []User
|
||||
err = db.Select(&users,
|
||||
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `active` = @@active",
|
||||
5*time.Minute, // Cache for 5 minutes
|
||||
true)
|
||||
duration1 := time.Since(start)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("First query failed: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" First query (cache miss): %v, %d users\n", duration1, len(users))
|
||||
|
||||
// Second query - cache hit
|
||||
start = time.Now()
|
||||
err = db.Select(&users,
|
||||
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `active` = @@active",
|
||||
5*time.Minute,
|
||||
true)
|
||||
duration2 := time.Since(start)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Second query failed: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Second query (cache hit): %v, %d users\n", duration2, len(users))
|
||||
fmt.Printf(" Speedup: %.2fx faster\n", float64(duration1)/float64(duration2))
|
||||
}
|
||||
|
||||
// redisCacheExample demonstrates Redis cache setup
|
||||
func redisCacheExample() {
|
||||
db, err := setupDatabase()
|
||||
if err != nil {
|
||||
log.Printf("Setup failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Setup Redis client
|
||||
redisClient := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
Password: "", // no password
|
||||
DB: 0, // default DB
|
||||
DialTimeout: 5 * time.Second,
|
||||
ReadTimeout: 3 * time.Second,
|
||||
WriteTimeout: 3 * time.Second,
|
||||
PoolSize: 10,
|
||||
MinIdleConns: 5,
|
||||
})
|
||||
|
||||
// Test Redis connection
|
||||
ctx := context.Background()
|
||||
_, err = redisClient.Ping(ctx).Result()
|
||||
if err != nil {
|
||||
log.Printf("Redis connection failed: %v", err)
|
||||
log.Println(" Skipping Redis cache example")
|
||||
return
|
||||
}
|
||||
|
||||
// Enable Redis cache
|
||||
db.EnableRedis(redisClient)
|
||||
fmt.Println("✓ Redis cache enabled (distributed, with locking)")
|
||||
|
||||
// Query with caching
|
||||
var users []User
|
||||
err = db.Select(&users,
|
||||
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge",
|
||||
10*time.Minute, // Cache for 10 minutes
|
||||
18)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Redis cached query failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Cached %d users in Redis\n", len(users))
|
||||
fmt.Println(" ✓ Cache shared across all application instances")
|
||||
fmt.Println(" ✓ Distributed locking prevents cache stampedes")
|
||||
}
|
||||
|
||||
// redisClusterExample demonstrates Redis Cluster setup
|
||||
func redisClusterExample() {
|
||||
db, err := setupDatabase()
|
||||
if err != nil {
|
||||
log.Printf("Setup failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Setup Redis Cluster client
|
||||
redisCluster := redis.NewClusterClient(&redis.ClusterOptions{
|
||||
Addrs: []string{
|
||||
"localhost:7000",
|
||||
"localhost:7001",
|
||||
"localhost:7002",
|
||||
},
|
||||
DialTimeout: 5 * time.Second,
|
||||
ReadTimeout: 3 * time.Second,
|
||||
WriteTimeout: 3 * time.Second,
|
||||
PoolSize: 10,
|
||||
})
|
||||
|
||||
// Test cluster connection
|
||||
ctx := context.Background()
|
||||
_, err = redisCluster.Ping(ctx).Result()
|
||||
if err != nil {
|
||||
log.Printf("Redis cluster connection failed: %v", err)
|
||||
log.Println(" Skipping Redis cluster example")
|
||||
return
|
||||
}
|
||||
|
||||
// Enable Redis cluster cache
|
||||
// Note: EnableRedis works with both single-node and cluster
|
||||
db.EnableRedis(redisCluster)
|
||||
fmt.Println("✓ Redis Cluster cache enabled")
|
||||
|
||||
// Query with caching
|
||||
var users []User
|
||||
err = db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` LIMIT @@limit", 5*time.Minute, 100)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Cluster cached query failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Cached %d users in Redis Cluster\n", len(users))
|
||||
}
|
||||
|
||||
// memcachedCacheExample demonstrates Memcached cache setup
|
||||
func memcachedCacheExample() {
|
||||
db, err := setupDatabase()
|
||||
if err != nil {
|
||||
log.Printf("Setup failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Setup Memcached client
|
||||
memcacheClient := memcache.New("localhost:11211")
|
||||
memcacheClient.Timeout = 3 * time.Second
|
||||
memcacheClient.MaxIdleConns = 10
|
||||
|
||||
// Test Memcached connection
|
||||
err = memcacheClient.Ping()
|
||||
if err != nil {
|
||||
log.Printf("Memcached connection failed: %v", err)
|
||||
log.Println(" Skipping Memcached example")
|
||||
return
|
||||
}
|
||||
|
||||
// Enable Memcached
|
||||
db.EnableMemcache(memcacheClient)
|
||||
fmt.Println("✓ Memcached cache enabled (distributed, simple)")
|
||||
|
||||
// Query with caching
|
||||
var users []User
|
||||
err = db.Select(&users,
|
||||
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `active` = @@active",
|
||||
15*time.Minute, // Cache for 15 minutes
|
||||
true)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Memcached query failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Cached %d users in Memcached\n", len(users))
|
||||
fmt.Println(" ⚠ No distributed locking (potential cache stampedes)")
|
||||
}
|
||||
|
||||
// multiCacheExample demonstrates multi-level caching
|
||||
func multiCacheExample() {
|
||||
db, err := setupDatabase()
|
||||
if err != nil {
|
||||
log.Printf("Setup failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Setup Redis
|
||||
redisClient := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
_, err = redisClient.Ping(ctx).Result()
|
||||
if err != nil {
|
||||
log.Printf("Redis unavailable, using weak cache only")
|
||||
db.UseCache(mysql.NewWeakCache())
|
||||
return
|
||||
}
|
||||
|
||||
// Create multi-level cache
|
||||
// L1: Fast local weak cache
|
||||
// L2: Shared Redis cache
|
||||
multiCache := mysql.NewMultiCache(
|
||||
mysql.NewWeakCache(), // L1: In-memory
|
||||
mysql.NewRedisCache(redisClient), // L2: Redis
|
||||
)
|
||||
|
||||
db.UseCache(multiCache)
|
||||
fmt.Println("✓ Multi-level cache enabled")
|
||||
fmt.Println(" L1: In-memory weak cache (fastest)")
|
||||
fmt.Println(" L2: Redis distributed cache (shared)")
|
||||
|
||||
// First query - cold cache (misses both levels)
|
||||
start := time.Now()
|
||||
var users []User
|
||||
err = db.Select(&users,
|
||||
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge",
|
||||
10*time.Minute,
|
||||
21)
|
||||
cold := time.Since(start)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Cold cache query failed: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("\n Cold cache (DB query): %v, %d users\n", cold, len(users))
|
||||
|
||||
// Second query - warm cache (hits L1)
|
||||
start = time.Now()
|
||||
err = db.Select(&users,
|
||||
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge",
|
||||
10*time.Minute,
|
||||
21)
|
||||
warm := time.Since(start)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Warm cache query failed: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" Warm cache (L1 hit): %v\n", warm)
|
||||
fmt.Printf(" Speedup: %.2fx faster\n", float64(cold)/float64(warm))
|
||||
}
|
||||
|
||||
// cacheStrategiesExample demonstrates different caching strategies
|
||||
func cacheStrategiesExample() {
|
||||
db, err := setupDatabase()
|
||||
if err != nil {
|
||||
log.Printf("Setup failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
db.UseCache(mysql.NewWeakCache())
|
||||
|
||||
// Strategy 1: No caching for real-time data
|
||||
fmt.Println("\nStrategy 1: No caching (TTL = 0)")
|
||||
var liveUsers []User
|
||||
err = db.Select(&liveUsers,
|
||||
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `last_active` > @@since",
|
||||
0, // No caching
|
||||
time.Now().Add(-5*time.Minute))
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Live query failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf(" ✓ %d active users (always fresh)\n", len(liveUsers))
|
||||
}
|
||||
|
||||
// Strategy 2: Short TTL for frequently changing data
|
||||
fmt.Println("\nStrategy 2: Short TTL (30 seconds)")
|
||||
err = db.Select(&liveUsers,
|
||||
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `active` = @@active",
|
||||
30*time.Second, // Short TTL
|
||||
true)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Short TTL query failed: %v", err)
|
||||
} else {
|
||||
fmt.Println(" ✓ Balance freshness and performance")
|
||||
}
|
||||
|
||||
// Strategy 3: Long TTL for reference data
|
||||
fmt.Println("\nStrategy 3: Long TTL (1 hour)")
|
||||
type Country struct {
|
||||
ID int `mysql:"id"`
|
||||
Name string `mysql:"name"`
|
||||
Code string `mysql:"code"`
|
||||
}
|
||||
|
||||
var countries []Country
|
||||
err = db.Select(&countries,
|
||||
"SELECT `id`, `name`, `code` FROM `countries`",
|
||||
time.Hour, // Long TTL for reference data
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Long TTL query failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf(" ✓ %d countries (rarely changes)\n", len(countries))
|
||||
}
|
||||
|
||||
// Strategy 4: Conditional caching based on result size
|
||||
fmt.Println("\nStrategy 4: Conditional caching")
|
||||
conditionalCacheQuery(db)
|
||||
|
||||
// Strategy 5: Read-after-write with SelectWrites
|
||||
fmt.Println("\nStrategy 5: Read-after-write consistency")
|
||||
readAfterWriteExample(db)
|
||||
}
|
||||
|
||||
// conditionalCacheQuery demonstrates dynamic TTL selection
|
||||
func conditionalCacheQuery(db *mysql.Database) {
|
||||
// First query to check result size
|
||||
var users []User
|
||||
err := db.Select(&users,
|
||||
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `status` = @@status",
|
||||
0, // No cache for initial check
|
||||
"active")
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Initial query failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Choose TTL based on result size
|
||||
var ttl time.Duration
|
||||
if len(users) > 1000 {
|
||||
ttl = 30 * time.Minute // Large result - cache longer
|
||||
fmt.Println(" Large result set (>1000) - using 30min TTL")
|
||||
} else if len(users) > 100 {
|
||||
ttl = 10 * time.Minute // Medium result - moderate TTL
|
||||
fmt.Println(" Medium result set (100-1000) - using 10min TTL")
|
||||
} else {
|
||||
ttl = 2 * time.Minute // Small result - short TTL
|
||||
fmt.Println(" Small result set (<100) - using 2min TTL")
|
||||
}
|
||||
|
||||
// Re-query with appropriate TTL
|
||||
err = db.Select(&users,
|
||||
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `status` = @@status",
|
||||
ttl,
|
||||
"active")
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Cached query failed: %v", err)
|
||||
} else {
|
||||
fmt.Printf(" ✓ %d users cached with TTL=%v\n", len(users), ttl)
|
||||
}
|
||||
}
|
||||
|
||||
// readAfterWriteExample demonstrates read-after-write pattern
|
||||
func readAfterWriteExample(db *mysql.Database) {
|
||||
// Insert new user
|
||||
newUser := User{
|
||||
Name: "CacheUser",
|
||||
Email: "cache@example.com",
|
||||
Age: 28,
|
||||
Active: true,
|
||||
}
|
||||
|
||||
err := db.Insert("users", newUser)
|
||||
if err != nil {
|
||||
log.Printf("Insert failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(" User inserted")
|
||||
|
||||
// WRONG: Using Select() might read from stale cache or replica
|
||||
// var user User
|
||||
// db.Select(&user, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `email` = @@email",
|
||||
// 5*time.Minute, mysql.Params{"email": "cache@example.com"})
|
||||
|
||||
// CORRECT: Use SelectWrites for read-after-write consistency
|
||||
var user User
|
||||
err = db.SelectWrites(&user,
|
||||
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `email` = @@email",
|
||||
0, // Don't cache write-pool reads
|
||||
"cache@example.com")
|
||||
|
||||
if err != nil {
|
||||
log.Printf("SelectWrites failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" ✓ User retrieved immediately (ID: %d)\n", user.ID)
|
||||
fmt.Println(" ✓ Used write pool for consistency")
|
||||
}
|
||||
|
||||
// performanceBenchmark compares cache vs no-cache performance
|
||||
func performanceBenchmark(db *mysql.Database) {
|
||||
fmt.Println("\nPerformance Benchmark: Cache vs No-Cache")
|
||||
|
||||
query := "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `active` = @@active"
|
||||
param := true
|
||||
|
||||
// Benchmark without cache
|
||||
start := time.Now()
|
||||
iterations := 100
|
||||
for i := 0; i < iterations; i++ {
|
||||
var users []User
|
||||
db.Select(&users, query, 0, param) // No cache
|
||||
}
|
||||
noCacheDuration := time.Since(start)
|
||||
avgNoCache := noCacheDuration / time.Duration(iterations)
|
||||
|
||||
fmt.Printf(" Without cache: %v total, %v avg per query\n",
|
||||
noCacheDuration, avgNoCache)
|
||||
|
||||
// Enable cache
|
||||
db.UseCache(mysql.NewWeakCache())
|
||||
|
||||
// Warm up cache
|
||||
var warmup []User
|
||||
db.Select(&warmup, query, 5*time.Minute, param)
|
||||
|
||||
// Benchmark with cache
|
||||
start = time.Now()
|
||||
for i := 0; i < iterations; i++ {
|
||||
var users []User
|
||||
db.Select(&users, query, 5*time.Minute, param) // With cache
|
||||
}
|
||||
cacheDuration := time.Since(start)
|
||||
avgCache := cacheDuration / time.Duration(iterations)
|
||||
|
||||
fmt.Printf(" With cache: %v total, %v avg per query\n",
|
||||
cacheDuration, avgCache)
|
||||
fmt.Printf(" Speedup: %.2fx faster with cache\n",
|
||||
float64(noCacheDuration)/float64(cacheDuration))
|
||||
}
|
||||
|
||||
// cacheKeyDebug demonstrates understanding cache keys
|
||||
func cacheKeyDebug(db *mysql.Database) {
|
||||
fmt.Println("\nCache Key Understanding")
|
||||
|
||||
db.UseCache(mysql.NewWeakCache())
|
||||
|
||||
// Same query, same params = same cache key
|
||||
fmt.Println(" 1. Identical queries share cache:")
|
||||
var users1, users2 []User
|
||||
|
||||
db.Select(&users1, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge", 5*time.Minute,
|
||||
18)
|
||||
|
||||
// This hits cache
|
||||
db.Select(&users2, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge", 5*time.Minute,
|
||||
18)
|
||||
|
||||
fmt.Println(" ✓ Second query used cached result")
|
||||
|
||||
// Different params = different cache key
|
||||
fmt.Println("\n 2. Different params = different cache:")
|
||||
var users3 []User
|
||||
db.Select(&users3, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge", 5*time.Minute,
|
||||
25) // Different param value
|
||||
|
||||
fmt.Println(" ✓ Different parameters bypass cache")
|
||||
|
||||
// Parameter order doesn't matter
|
||||
fmt.Println("\n 3. Parameter order normalized:")
|
||||
var users4, users5 []User
|
||||
|
||||
db.Select(&users4,
|
||||
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge AND `active` = @@active",
|
||||
5*time.Minute,
|
||||
mysql.Params{"minAge": 18, "active": true})
|
||||
|
||||
// Same cache even though params in different order
|
||||
db.Select(&users5,
|
||||
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge AND `active` = @@active",
|
||||
5*time.Minute,
|
||||
mysql.Params{"active": true, "minAge": 18}) // Reversed
|
||||
|
||||
fmt.Println(" ✓ Parameter order doesn't affect cache key")
|
||||
}
|
||||
611
examples/transaction-patterns.go
Normal file
611
examples/transaction-patterns.go
Normal file
@@ -0,0 +1,611 @@
|
||||
// Package examples demonstrates transaction patterns with cool-mysql
|
||||
package examples
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
mysql "github.com/StirlingMarketingGroup/cool-mysql"
|
||||
)
|
||||
|
||||
// TransactionExamples demonstrates transaction handling patterns
|
||||
func TransactionExamples() {
|
||||
fmt.Println("=== TRANSACTION EXAMPLES ===")
|
||||
|
||||
db, err := setupDatabase()
|
||||
if err != nil {
|
||||
log.Printf("Setup failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Basic transaction
|
||||
fmt.Println("\n1. Basic Transaction")
|
||||
basicTransactionExample()
|
||||
|
||||
// Nested transaction handling
|
||||
fmt.Println("\n2. Nested Transaction (Context-Based)")
|
||||
nestedTransactionExample()
|
||||
|
||||
// Rollback on error
|
||||
fmt.Println("\n3. Automatic Rollback on Error")
|
||||
rollbackExample()
|
||||
|
||||
// Complex transaction
|
||||
fmt.Println("\n4. Complex Multi-Step Transaction")
|
||||
complexTransactionExample()
|
||||
|
||||
// Batch transaction
|
||||
fmt.Println("\n5. Batch Transaction")
|
||||
batchTransactionExample(context.Background(), db)
|
||||
|
||||
// Transaction with retry
|
||||
fmt.Println("\n6. Transaction with Retry Logic")
|
||||
transactionWithRetry(context.Background(), db)
|
||||
|
||||
// Savepoint example
|
||||
fmt.Println("\n7. Savepoint Pattern")
|
||||
savepointExample(context.Background(), db)
|
||||
|
||||
// Isolation level
|
||||
fmt.Println("\n8. Custom Isolation Level")
|
||||
isolationLevelExample(context.Background(), db)
|
||||
}
|
||||
|
||||
// basicTransactionExample demonstrates basic transaction usage
|
||||
func basicTransactionExample() {
|
||||
db, err := setupDatabase()
|
||||
if err != nil {
|
||||
log.Printf("Setup failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Get or create transaction
|
||||
tx, commit, cancel, err := mysql.GetOrCreateTxFromContext(ctx)
|
||||
defer cancel() // Always safe to call - rolls back if commit() not called
|
||||
if err != nil {
|
||||
log.Printf("Failed to create transaction: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Store transaction in context
|
||||
ctx = mysql.NewContextWithTx(ctx, tx)
|
||||
|
||||
// Execute operations in transaction
|
||||
user := User{
|
||||
Name: "TxUser1",
|
||||
Email: "tx1@example.com",
|
||||
Age: 30,
|
||||
Active: true,
|
||||
}
|
||||
|
||||
err = db.Insert("users", user)
|
||||
if err != nil {
|
||||
log.Printf("Insert failed: %v", err)
|
||||
return // cancel() will rollback
|
||||
}
|
||||
|
||||
fmt.Println(" User inserted in transaction")
|
||||
|
||||
// Update in same transaction
|
||||
err = db.Exec("UPDATE `users` SET `age` = @@age WHERE `email` = @@email",
|
||||
mysql.Params{
|
||||
"age": 31,
|
||||
"email": "tx1@example.com",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Update failed: %v", err)
|
||||
return // cancel() will rollback
|
||||
}
|
||||
|
||||
fmt.Println(" User updated in transaction")
|
||||
|
||||
// Commit transaction
|
||||
if err := commit(); err != nil {
|
||||
log.Printf("Commit failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("✓ Transaction committed successfully")
|
||||
}
|
||||
|
||||
// nestedTransactionExample demonstrates nested transaction handling
|
||||
func nestedTransactionExample() {
|
||||
db, err := setupDatabase()
|
||||
if err != nil {
|
||||
log.Printf("Setup failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Start outer transaction
|
||||
err = outerTransaction(ctx, db)
|
||||
if err != nil {
|
||||
log.Printf("Outer transaction failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("✓ Nested transactions completed")
|
||||
}
|
||||
|
||||
func outerTransaction(ctx context.Context, db *mysql.Database) error {
|
||||
// Get or create transaction
|
||||
tx, commit, cancel, err := mysql.GetOrCreateTxFromContext(ctx)
|
||||
defer cancel()
|
||||
if err != nil {
|
||||
return fmt.Errorf("outer tx failed: %w", err)
|
||||
}
|
||||
|
||||
// Store in context
|
||||
ctx = mysql.NewContextWithTx(ctx, tx)
|
||||
|
||||
fmt.Println(" Started outer transaction")
|
||||
|
||||
// Insert user
|
||||
user := User{
|
||||
Name: "OuterTxUser",
|
||||
Email: "outer@example.com",
|
||||
Age: 25,
|
||||
Active: true,
|
||||
}
|
||||
|
||||
err = db.Insert("users", user)
|
||||
if err != nil {
|
||||
return fmt.Errorf("outer insert failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(" Outer: User inserted")
|
||||
|
||||
// Call inner function with same context
|
||||
// GetOrCreateTxFromContext will return existing transaction
|
||||
err = innerTransaction(ctx, db)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inner tx failed: %w", err)
|
||||
}
|
||||
|
||||
// Commit outer transaction
|
||||
if err := commit(); err != nil {
|
||||
return fmt.Errorf("outer commit failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(" Outer transaction committed")
|
||||
return nil
|
||||
}
|
||||
|
||||
func innerTransaction(ctx context.Context, db *mysql.Database) error {
|
||||
// Get or create transaction (will reuse existing from context)
|
||||
tx, commit, cancel, err := mysql.GetOrCreateTxFromContext(ctx)
|
||||
defer cancel()
|
||||
if err != nil {
|
||||
return fmt.Errorf("inner tx failed: %w", err)
|
||||
}
|
||||
|
||||
// Transaction already in context, so this is a no-op
|
||||
ctx = mysql.NewContextWithTx(ctx, tx)
|
||||
|
||||
fmt.Println(" Inner: Reusing outer transaction")
|
||||
|
||||
// Update user
|
||||
err = db.Exec("UPDATE `users` SET `age` = @@age WHERE `email` = @@email",
|
||||
mysql.Params{
|
||||
"age": 26,
|
||||
"email": "outer@example.com",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("inner update failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(" Inner: User updated")
|
||||
|
||||
// Commit (safe to call, won't actually commit until outer commits)
|
||||
if err := commit(); err != nil {
|
||||
return fmt.Errorf("inner commit failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(" Inner: Operations complete")
|
||||
return nil
|
||||
}
|
||||
|
||||
// rollbackExample demonstrates automatic rollback on error
|
||||
func rollbackExample() {
|
||||
db, err := setupDatabase()
|
||||
if err != nil {
|
||||
log.Printf("Setup failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
err = failingTransaction(ctx, db)
|
||||
if err != nil {
|
||||
fmt.Printf(" Transaction failed as expected: %v\n", err)
|
||||
fmt.Println("✓ Transaction automatically rolled back")
|
||||
}
|
||||
|
||||
// Verify rollback - user should not exist
|
||||
var user User
|
||||
err = db.Select(&user,
|
||||
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `email` = @@email",
|
||||
0,
|
||||
"rollback@example.com")
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
fmt.Println("✓ Verified: User was not inserted (rollback worked)")
|
||||
} else if err != nil {
|
||||
log.Printf("Verification query failed: %v", err)
|
||||
} else {
|
||||
log.Println("✗ Error: User exists (rollback failed)")
|
||||
}
|
||||
}
|
||||
|
||||
func failingTransaction(ctx context.Context, db *mysql.Database) error {
|
||||
tx, _, cancel, err := mysql.GetOrCreateTxFromContext(ctx)
|
||||
defer cancel() // Will rollback since we don't call commit
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx = mysql.NewContextWithTx(ctx, tx)
|
||||
|
||||
// Insert user
|
||||
user := User{
|
||||
Name: "RollbackUser",
|
||||
Email: "rollback@example.com",
|
||||
Age: 28,
|
||||
Active: true,
|
||||
}
|
||||
|
||||
err = db.Insert("users", user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(" User inserted")
|
||||
|
||||
// Simulate error before commit
|
||||
fmt.Println(" Simulating error...")
|
||||
return fmt.Errorf("simulated error - transaction will rollback")
|
||||
|
||||
// commit() is never called, so cancel() will rollback
|
||||
}
|
||||
|
||||
// complexTransactionExample demonstrates a complex multi-step transaction
|
||||
func complexTransactionExample() {
|
||||
db, err := setupDatabase()
|
||||
if err != nil {
|
||||
log.Printf("Setup failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
err = transferFunds(ctx, db, "user1@example.com", "user2@example.com", 100)
|
||||
if err != nil {
|
||||
log.Printf("Transfer failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("✓ Complex transaction completed")
|
||||
}
|
||||
|
||||
// transferFunds demonstrates a bank transfer-like transaction
|
||||
func transferFunds(ctx context.Context, db *mysql.Database, fromEmail, toEmail string, amount int) error {
|
||||
tx, commit, cancel, err := mysql.GetOrCreateTxFromContext(ctx)
|
||||
defer cancel()
|
||||
if err != nil {
|
||||
return fmt.Errorf("transaction start failed: %w", err)
|
||||
}
|
||||
|
||||
ctx = mysql.NewContextWithTx(ctx, tx)
|
||||
|
||||
fmt.Printf(" Starting transfer: %d from %s to %s\n", amount, fromEmail, toEmail)
|
||||
|
||||
// Step 1: Check sender balance
|
||||
type Account struct {
|
||||
Email string `mysql:"email"`
|
||||
Balance int `mysql:"balance"`
|
||||
}
|
||||
|
||||
var sender Account
|
||||
err = db.SelectWrites(&sender,
|
||||
"SELECT email, balance FROM `accounts` WHERE `email` = @@email FOR UPDATE",
|
||||
0, // Use write pool for transaction
|
||||
fromEmail)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return fmt.Errorf("sender account not found")
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to fetch sender: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf(" Sender balance: %d\n", sender.Balance)
|
||||
|
||||
// Step 2: Verify sufficient funds
|
||||
if sender.Balance < amount {
|
||||
return fmt.Errorf("insufficient funds: have %d, need %d", sender.Balance, amount)
|
||||
}
|
||||
|
||||
// Step 3: Check receiver exists
|
||||
var receiver Account
|
||||
err = db.SelectWrites(&receiver,
|
||||
"SELECT email, balance FROM `accounts` WHERE `email` = @@email FOR UPDATE",
|
||||
0,
|
||||
toEmail)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return fmt.Errorf("receiver account not found")
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to fetch receiver: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf(" Receiver balance: %d\n", receiver.Balance)
|
||||
|
||||
// Step 4: Deduct from sender
|
||||
result, err := db.ExecResult(
|
||||
"UPDATE accounts SET `balance` = balance - @@amount WHERE `email` = @@email",
|
||||
mysql.Params{
|
||||
"amount": amount,
|
||||
"email": fromEmail,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to deduct from sender: %w", err)
|
||||
}
|
||||
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("sender update affected 0 rows")
|
||||
}
|
||||
|
||||
fmt.Printf(" Deducted %d from sender\n", amount)
|
||||
|
||||
// Step 5: Add to receiver
|
||||
result, err = db.ExecResult(
|
||||
"UPDATE accounts SET `balance` = balance + @@amount WHERE `email` = @@email",
|
||||
mysql.Params{
|
||||
"amount": amount,
|
||||
"email": toEmail,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add to receiver: %w", err)
|
||||
}
|
||||
|
||||
rows, _ = result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("receiver update affected 0 rows")
|
||||
}
|
||||
|
||||
fmt.Printf(" Added %d to receiver\n", amount)
|
||||
|
||||
// Step 6: Record transaction log
|
||||
type TransactionLog struct {
|
||||
FromEmail string `mysql:"from_email"`
|
||||
ToEmail string `mysql:"to_email"`
|
||||
Amount int `mysql:"amount"`
|
||||
}
|
||||
|
||||
txLog := TransactionLog{
|
||||
FromEmail: fromEmail,
|
||||
ToEmail: toEmail,
|
||||
Amount: amount,
|
||||
}
|
||||
|
||||
err = db.Insert("transaction_logs", txLog)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to log transaction: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(" Transaction logged")
|
||||
|
||||
// Step 7: Commit all changes atomically
|
||||
if err := commit(); err != nil {
|
||||
return fmt.Errorf("commit failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(" All changes committed atomically")
|
||||
return nil
|
||||
}
|
||||
|
||||
// transactionWithRetry demonstrates transaction retry pattern
|
||||
func transactionWithRetry(ctx context.Context, db *mysql.Database) error {
|
||||
maxRetries := 3
|
||||
var err error
|
||||
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
err = attemptTransaction(ctx, db)
|
||||
if err == nil {
|
||||
return nil // Success
|
||||
}
|
||||
|
||||
// Check if error is retryable (e.g., deadlock)
|
||||
if !isRetryableError(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf(" Attempt %d failed (retryable): %v\n", attempt, err)
|
||||
|
||||
if attempt < maxRetries {
|
||||
fmt.Printf(" Retrying... (%d/%d)\n", attempt+1, maxRetries)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("transaction failed after %d attempts: %w", maxRetries, err)
|
||||
}
|
||||
|
||||
func attemptTransaction(ctx context.Context, db *mysql.Database) error {
|
||||
tx, commit, cancel, err := mysql.GetOrCreateTxFromContext(ctx)
|
||||
defer cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx = mysql.NewContextWithTx(ctx, tx)
|
||||
|
||||
// Perform transaction operations...
|
||||
err = db.Insert("users", User{
|
||||
Name: "RetryUser",
|
||||
Email: "retry@example.com",
|
||||
Age: 29,
|
||||
Active: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return commit()
|
||||
}
|
||||
|
||||
func isRetryableError(err error) bool {
|
||||
// Check for MySQL deadlock or lock timeout errors
|
||||
// Note: cool-mysql already handles automatic retries for these
|
||||
// This is just an example of manual retry logic
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
errStr := err.Error()
|
||||
return strings.Contains(errStr, "deadlock") ||
|
||||
strings.Contains(errStr, "lock wait timeout") ||
|
||||
strings.Contains(errStr, "try restarting transaction")
|
||||
}
|
||||
|
||||
// Batch transaction example
|
||||
func batchTransactionExample(ctx context.Context, db *mysql.Database) error {
|
||||
fmt.Println("\nBatch Transaction Example")
|
||||
|
||||
tx, commit, cancel, err := mysql.GetOrCreateTxFromContext(ctx)
|
||||
defer cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx = mysql.NewContextWithTx(ctx, tx)
|
||||
|
||||
// Insert multiple users in single transaction
|
||||
users := []User{
|
||||
{Name: "BatchUser1", Email: "batch1@example.com", Age: 20, Active: true},
|
||||
{Name: "BatchUser2", Email: "batch2@example.com", Age: 21, Active: true},
|
||||
{Name: "BatchUser3", Email: "batch3@example.com", Age: 22, Active: true},
|
||||
}
|
||||
|
||||
err = db.Insert("users", users)
|
||||
if err != nil {
|
||||
return fmt.Errorf("batch insert failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf(" Inserted %d users in transaction\n", len(users))
|
||||
|
||||
// Commit
|
||||
if err := commit(); err != nil {
|
||||
return fmt.Errorf("batch commit failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("✓ Batch transaction committed")
|
||||
return nil
|
||||
}
|
||||
|
||||
// savepoint example (MySQL specific)
|
||||
func savepointExample(ctx context.Context, db *mysql.Database) error {
|
||||
fmt.Println("\nSavepoint Example (Advanced)")
|
||||
|
||||
tx, commit, cancel, err := mysql.GetOrCreateTxFromContext(ctx)
|
||||
defer cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx = mysql.NewContextWithTx(ctx, tx)
|
||||
|
||||
// Insert first user
|
||||
user1 := User{Name: "SavepointUser1", Email: "sp1@example.com", Age: 25, Active: true}
|
||||
err = db.Insert("users", user1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(" User 1 inserted")
|
||||
|
||||
// Create savepoint
|
||||
err = db.Exec("SAVEPOINT sp1")
|
||||
if err != nil {
|
||||
return fmt.Errorf("savepoint creation failed: %w", err)
|
||||
}
|
||||
fmt.Println(" Savepoint 'sp1' created")
|
||||
|
||||
// Insert second user
|
||||
user2 := User{Name: "SavepointUser2", Email: "sp2@example.com", Age: 26, Active: true}
|
||||
err = db.Insert("users", user2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(" User 2 inserted")
|
||||
|
||||
// Simulate error and rollback to savepoint
|
||||
fmt.Println(" Simulating error, rolling back to savepoint...")
|
||||
err = db.Exec("ROLLBACK TO SAVEPOINT sp1")
|
||||
if err != nil {
|
||||
return fmt.Errorf("rollback to savepoint failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(" Rolled back to savepoint (User 2 not saved)")
|
||||
|
||||
// Insert different user
|
||||
user3 := User{Name: "SavepointUser3", Email: "sp3@example.com", Age: 27, Active: true}
|
||||
err = db.Insert("users", user3)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(" User 3 inserted")
|
||||
|
||||
// Commit transaction
|
||||
if err := commit(); err != nil {
|
||||
return fmt.Errorf("commit failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("✓ Transaction committed (User 1 and 3 saved, User 2 rolled back)")
|
||||
return nil
|
||||
}
|
||||
|
||||
// readCommittedIsolation example
|
||||
func isolationLevelExample(ctx context.Context, db *mysql.Database) error {
|
||||
fmt.Println("\nIsolation Level Example")
|
||||
|
||||
// Set isolation level before transaction
|
||||
err := db.Exec("SET TRANSACTION ISOLATION LEVEL READ COMMITTED")
|
||||
if err != nil {
|
||||
return fmt.Errorf("set isolation level failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(" Isolation level set to READ COMMITTED")
|
||||
|
||||
tx, commit, cancel, err := mysql.GetOrCreateTxFromContext(ctx)
|
||||
defer cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx = mysql.NewContextWithTx(ctx, tx)
|
||||
|
||||
// Transaction operations...
|
||||
var users []User
|
||||
err = db.SelectWrites(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `active` = 1", 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf(" Read %d users with READ COMMITTED isolation\n", len(users))
|
||||
|
||||
if err := commit(); err != nil {
|
||||
return fmt.Errorf("commit failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
707
examples/upsert-examples.go
Normal file
707
examples/upsert-examples.go
Normal file
@@ -0,0 +1,707 @@
|
||||
// Package examples demonstrates upsert patterns with cool-mysql
|
||||
package examples
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
mysql "github.com/StirlingMarketingGroup/cool-mysql"
|
||||
)
|
||||
|
||||
// UpsertExamples demonstrates INSERT ... ON DUPLICATE KEY UPDATE patterns
|
||||
func UpsertExamples() {
|
||||
fmt.Println("=== UPSERT EXAMPLES ===")
|
||||
|
||||
// Basic upsert
|
||||
fmt.Println("\n1. Basic Upsert (by unique key)")
|
||||
basicUpsertExample()
|
||||
|
||||
// Upsert with multiple unique columns
|
||||
fmt.Println("\n2. Upsert with Composite Unique Key")
|
||||
compositeKeyUpsertExample()
|
||||
|
||||
// Conditional upsert
|
||||
fmt.Println("\n3. Conditional Upsert with WHERE clause")
|
||||
conditionalUpsertExample()
|
||||
|
||||
// Batch upsert
|
||||
fmt.Println("\n4. Batch Upsert")
|
||||
batchUpsertExample()
|
||||
|
||||
// Selective column updates
|
||||
fmt.Println("\n5. Selective Column Updates")
|
||||
selectiveUpdateExample()
|
||||
|
||||
// Timestamp tracking
|
||||
fmt.Println("\n6. Timestamp Tracking with Upsert")
|
||||
timestampUpsertExample()
|
||||
|
||||
// Increment counter
|
||||
fmt.Println("\n7. Increment Counter Pattern")
|
||||
incrementCounterExample()
|
||||
|
||||
// Upsert from channel
|
||||
fmt.Println("\n8. Upsert from Channel (streaming)")
|
||||
upsertFromChannelExample()
|
||||
|
||||
// Upsert vs Insert Ignore
|
||||
fmt.Println("\n9. Upsert vs Insert Ignore")
|
||||
UpsertOrIgnoreExample()
|
||||
}
|
||||
|
||||
// basicUpsertExample demonstrates simple upsert by email
|
||||
func basicUpsertExample() {
|
||||
db, err := setupDatabase()
|
||||
if err != nil {
|
||||
log.Printf("Setup failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Define user with unique email
|
||||
user := User{
|
||||
Name: "UpsertUser",
|
||||
Email: "upsert@example.com", // Unique key
|
||||
Age: 30,
|
||||
Active: true,
|
||||
}
|
||||
|
||||
// First upsert - INSERT (user doesn't exist)
|
||||
err = db.Upsert(
|
||||
"users", // table
|
||||
[]string{"email"}, // unique columns (conflict detection)
|
||||
[]string{"name", "age", "active"}, // columns to update on conflict
|
||||
"", // no WHERE clause
|
||||
user, // data
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("First upsert failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(" First upsert: User inserted")
|
||||
|
||||
// Second upsert - UPDATE (user exists)
|
||||
user.Name = "UpsertUser Updated"
|
||||
user.Age = 31
|
||||
|
||||
err = db.Upsert(
|
||||
"users",
|
||||
[]string{"email"},
|
||||
[]string{"name", "age", "active"},
|
||||
"",
|
||||
user,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Second upsert failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(" Second upsert: User updated")
|
||||
|
||||
// Verify result
|
||||
var retrieved User
|
||||
err = db.Select(&retrieved,
|
||||
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `email` = @@email",
|
||||
0,
|
||||
"upsert@example.com")
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Verification failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Final state: Name='%s', Age=%d\n", retrieved.Name, retrieved.Age)
|
||||
}
|
||||
|
||||
// compositeKeyUpsertExample demonstrates upsert with multiple unique columns
|
||||
func compositeKeyUpsertExample() {
|
||||
db, err := setupDatabase()
|
||||
if err != nil {
|
||||
log.Printf("Setup failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
type ProductInventory struct {
|
||||
StoreID int `mysql:"store_id"`
|
||||
ProductID int `mysql:"product_id"`
|
||||
Quantity int `mysql:"quantity"`
|
||||
Price float64 `mysql:"price"`
|
||||
}
|
||||
|
||||
// Initial inventory
|
||||
inventory := ProductInventory{
|
||||
StoreID: 1,
|
||||
ProductID: 100,
|
||||
Quantity: 50,
|
||||
Price: 19.99,
|
||||
}
|
||||
|
||||
// First upsert - INSERT
|
||||
err = db.Upsert(
|
||||
"inventory",
|
||||
[]string{"store_id", "product_id"}, // Composite unique key
|
||||
[]string{"quantity", "price"}, // Update these on conflict
|
||||
"",
|
||||
inventory,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("First upsert failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(" Inventory created: Store 1, Product 100, Qty=50, Price=$19.99")
|
||||
|
||||
// Second upsert - UPDATE existing inventory
|
||||
inventory.Quantity = 75
|
||||
inventory.Price = 17.99
|
||||
|
||||
err = db.Upsert(
|
||||
"inventory",
|
||||
[]string{"store_id", "product_id"},
|
||||
[]string{"quantity", "price"},
|
||||
"",
|
||||
inventory,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Second upsert failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(" Inventory updated: Qty=75, Price=$17.99")
|
||||
fmt.Println("✓ Composite key upsert successful")
|
||||
}
|
||||
|
||||
// conditionalUpsertExample demonstrates upsert with WHERE clause
|
||||
func conditionalUpsertExample() {
|
||||
db, err := setupDatabase()
|
||||
if err != nil {
|
||||
log.Printf("Setup failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
type Document struct {
|
||||
ID int `mysql:"id"`
|
||||
Title string `mysql:"title"`
|
||||
Content string `mysql:"content"`
|
||||
Version int `mysql:"version"`
|
||||
UpdatedAt time.Time `mysql:"updated_at"`
|
||||
}
|
||||
|
||||
// Initial document
|
||||
doc := Document{
|
||||
ID: 1,
|
||||
Title: "My Document",
|
||||
Content: "Version 1 content",
|
||||
Version: 1,
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Insert initial version
|
||||
err = db.Insert("documents", doc)
|
||||
if err != nil {
|
||||
log.Printf("Insert failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(" Document v1 created")
|
||||
|
||||
// Upsert with condition: only update if newer
|
||||
doc.Content = "Version 2 content"
|
||||
doc.Version = 2
|
||||
doc.UpdatedAt = time.Now()
|
||||
|
||||
err = db.Upsert(
|
||||
"documents",
|
||||
[]string{"id"},
|
||||
[]string{"title", "content", "version", "updated_at"},
|
||||
"version < VALUES(version)", // Only update if new version is higher
|
||||
doc,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Conditional upsert failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(" Document updated to v2 (condition met)")
|
||||
|
||||
// Try to upsert with older version (should not update)
|
||||
oldDoc := Document{
|
||||
ID: 1,
|
||||
Title: "My Document",
|
||||
Content: "Old content",
|
||||
Version: 1, // Older version
|
||||
UpdatedAt: time.Now().Add(-time.Hour),
|
||||
}
|
||||
|
||||
err = db.Upsert(
|
||||
"documents",
|
||||
[]string{"id"},
|
||||
[]string{"title", "content", "version", "updated_at"},
|
||||
"version < VALUES(version)",
|
||||
oldDoc,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Old version upsert failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(" Old version upsert executed (but condition prevented update)")
|
||||
|
||||
// Verify current version
|
||||
var current Document
|
||||
err = db.Select(¤t,
|
||||
"SELECT `id`, `title`, `content`, `version`, `updated_at` FROM `documents` WHERE `id` = @@id",
|
||||
0,
|
||||
1)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Verification failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Final version: %d (conditional update worked)\n", current.Version)
|
||||
}
|
||||
|
||||
// batchUpsertExample demonstrates upserting multiple records
|
||||
func batchUpsertExample() {
|
||||
db, err := setupDatabase()
|
||||
if err != nil {
|
||||
log.Printf("Setup failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
type Setting struct {
|
||||
Key string `mysql:"key"`
|
||||
Value string `mysql:"value"`
|
||||
}
|
||||
|
||||
// Batch of settings
|
||||
settings := []Setting{
|
||||
{Key: "theme", Value: "dark"},
|
||||
{Key: "language", Value: "en"},
|
||||
{Key: "notifications", Value: "enabled"},
|
||||
{Key: "timezone", Value: "UTC"},
|
||||
}
|
||||
|
||||
// Upsert all settings
|
||||
err = db.Upsert(
|
||||
"settings",
|
||||
[]string{"key"}, // Unique on key
|
||||
[]string{"value"}, // Update value on conflict
|
||||
"",
|
||||
settings,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Batch upsert failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Inserted/updated %d settings\n", len(settings))
|
||||
|
||||
// Update some settings
|
||||
updatedSettings := []Setting{
|
||||
{Key: "theme", Value: "light"}, // Changed
|
||||
{Key: "language", Value: "es"}, // Changed
|
||||
{Key: "notifications", Value: "enabled"}, // Same
|
||||
{Key: "font_size", Value: "14"}, // New
|
||||
}
|
||||
|
||||
err = db.Upsert(
|
||||
"settings",
|
||||
[]string{"key"},
|
||||
[]string{"value"},
|
||||
"",
|
||||
updatedSettings,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Update batch upsert failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Updated batch: 2 changed, 1 same, 1 new\n")
|
||||
|
||||
// Verify results
|
||||
var allSettings []Setting
|
||||
err = db.Select(&allSettings,
|
||||
"SELECT `key`, `value` FROM `settings` ORDER BY key",
|
||||
0)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Verification failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Total settings: %d\n", len(allSettings))
|
||||
for _, s := range allSettings {
|
||||
fmt.Printf(" %s = %s\n", s.Key, s.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// selectiveUpdateExample demonstrates updating only specific columns
|
||||
func selectiveUpdateExample() {
|
||||
db, err := setupDatabase()
|
||||
if err != nil {
|
||||
log.Printf("Setup failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
type UserProfile struct {
|
||||
Email string `mysql:"email"`
|
||||
Name string `mysql:"name"`
|
||||
Bio string `mysql:"bio"`
|
||||
Avatar string `mysql:"avatar"`
|
||||
UpdatedAt time.Time `mysql:"updated_at,defaultzero"`
|
||||
}
|
||||
|
||||
// Initial profile
|
||||
profile := UserProfile{
|
||||
Email: "profile@example.com",
|
||||
Name: "John Doe",
|
||||
Bio: "Software Developer",
|
||||
Avatar: "avatar1.jpg",
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = db.Insert("user_profiles", profile)
|
||||
if err != nil {
|
||||
log.Printf("Insert failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(" Profile created")
|
||||
|
||||
// Update only name and bio (not avatar)
|
||||
profile.Name = "John Smith"
|
||||
profile.Bio = "Senior Software Developer"
|
||||
// Avatar unchanged
|
||||
|
||||
err = db.Upsert(
|
||||
"user_profiles",
|
||||
[]string{"email"},
|
||||
[]string{"name", "bio", "updated_at"}, // Don't include avatar
|
||||
"",
|
||||
profile,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Selective update failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(" Updated name and bio (avatar unchanged)")
|
||||
|
||||
// Later, update only avatar
|
||||
profile.Avatar = "avatar2.jpg"
|
||||
|
||||
err = db.Upsert(
|
||||
"user_profiles",
|
||||
[]string{"email"},
|
||||
[]string{"avatar", "updated_at"}, // Only avatar
|
||||
"",
|
||||
profile,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Avatar update failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(" Updated avatar only")
|
||||
|
||||
// Verify
|
||||
var final UserProfile
|
||||
err = db.Select(&final,
|
||||
"SELECT `email`, `name`, `bio`, `avatar`, `updated_at` FROM `user_profiles` WHERE `email` = @@email",
|
||||
0,
|
||||
"profile@example.com")
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Verification failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Final: Name='%s', Bio='%s', Avatar='%s'\n",
|
||||
final.Name, final.Bio, final.Avatar)
|
||||
}
|
||||
|
||||
// timestampUpsertExample demonstrates tracking created_at and updated_at
|
||||
func timestampUpsertExample() {
|
||||
db, err := setupDatabase()
|
||||
if err != nil {
|
||||
log.Printf("Setup failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
type Article struct {
|
||||
Slug string `mysql:"slug"`
|
||||
Title string `mysql:"title"`
|
||||
Content string `mysql:"content"`
|
||||
Views int `mysql:"views"`
|
||||
CreatedAt time.Time `mysql:"created_at,defaultzero"`
|
||||
UpdatedAt time.Time `mysql:"updated_at,defaultzero"`
|
||||
}
|
||||
|
||||
// Initial article
|
||||
article := Article{
|
||||
Slug: "my-article",
|
||||
Title: "My Article",
|
||||
Content: "Initial content",
|
||||
Views: 0,
|
||||
}
|
||||
|
||||
// First upsert - INSERT
|
||||
// CreatedAt and UpdatedAt will use database defaults
|
||||
err = db.Upsert(
|
||||
"articles",
|
||||
[]string{"slug"},
|
||||
[]string{"title", "content", "views", "updated_at"},
|
||||
"",
|
||||
article,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Insert failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(" Article created (created_at set by DB)")
|
||||
|
||||
// Get the article to see timestamps
|
||||
var inserted Article
|
||||
err = db.Select(&inserted,
|
||||
"SELECT `slug`, `title`, `content`, `views`, `created_at`, `updated_at` FROM `articles` WHERE slug = @@slug",
|
||||
0,
|
||||
"my-article")
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Select failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Created at: %s\n", inserted.CreatedAt.Format(time.RFC3339))
|
||||
|
||||
time.Sleep(2 * time.Second) // Wait to see time difference
|
||||
|
||||
// Update article
|
||||
article.Title = "My Updated Article"
|
||||
article.Content = "Updated content"
|
||||
article.Views = inserted.Views + 10
|
||||
|
||||
err = db.Upsert(
|
||||
"articles",
|
||||
[]string{"slug"},
|
||||
[]string{"title", "content", "views", "updated_at"},
|
||||
"", // Don't update created_at
|
||||
article,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Update failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(" Article updated (updated_at changed, created_at preserved)")
|
||||
|
||||
// Verify timestamps
|
||||
var updated Article
|
||||
err = db.Select(&updated,
|
||||
"SELECT `slug`, `title`, `content`, `views`, `created_at`, `updated_at` FROM `articles` WHERE slug = @@slug",
|
||||
0,
|
||||
"my-article")
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Verification failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Created at: %s (unchanged)\n", updated.CreatedAt.Format(time.RFC3339))
|
||||
fmt.Printf(" Updated at: %s (newer)\n", updated.UpdatedAt.Format(time.RFC3339))
|
||||
fmt.Printf("✓ Timestamps tracked correctly\n")
|
||||
}
|
||||
|
||||
// incrementCounterExample demonstrates atomic counter updates
|
||||
func incrementCounterExample() {
|
||||
db, err := setupDatabase()
|
||||
if err != nil {
|
||||
log.Printf("Setup failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("\nIncrement Counter Example")
|
||||
|
||||
type PageView struct {
|
||||
Page string `mysql:"page"`
|
||||
Views int `mysql:"views"`
|
||||
}
|
||||
|
||||
page := "homepage"
|
||||
|
||||
// Increment views (insert with 1 if not exists, increment if exists)
|
||||
// This uses MySQL's VALUES() function to reference the insert value
|
||||
viewRecord := PageView{
|
||||
Page: page,
|
||||
Views: 1, // Initial value for insert
|
||||
}
|
||||
|
||||
// Custom upsert to increment on duplicate
|
||||
err = db.Exec(
|
||||
"INSERT INTO `page_views` (`page`, `views`)"+
|
||||
" VALUES (@@page, @@views)"+
|
||||
" ON DUPLICATE KEY UPDATE `views` = `views` + @@views",
|
||||
mysql.Params{
|
||||
"page": viewRecord.Page,
|
||||
"views": viewRecord.Views,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Increment failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" View count incremented for %s\n", page)
|
||||
|
||||
// Increment again
|
||||
err = db.Exec(
|
||||
"INSERT INTO `page_views` (`page`, `views`)"+
|
||||
" VALUES (@@page, @@views)"+
|
||||
" ON DUPLICATE KEY UPDATE `views` = `views` + @@views",
|
||||
mysql.Params{
|
||||
"page": page,
|
||||
"views": 1,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Second increment failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check total views
|
||||
var totalViews int
|
||||
err = db.Select(&totalViews,
|
||||
"SELECT views FROM `page_views` WHERE page = @@page",
|
||||
0,
|
||||
page)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Select views failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Total views for %s: %d\n", page, totalViews)
|
||||
}
|
||||
|
||||
// upsertFromChannelExample demonstrates streaming upserts
|
||||
func upsertFromChannelExample() {
|
||||
db, err := setupDatabase()
|
||||
if err != nil {
|
||||
log.Printf("Setup failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("\nUpsert from Channel Example")
|
||||
|
||||
type Metric struct {
|
||||
MetricName string `mysql:"metric_name"`
|
||||
Value float64 `mysql:"value"`
|
||||
Timestamp time.Time `mysql:"timestamp,defaultzero"`
|
||||
}
|
||||
|
||||
// Create channel of metrics
|
||||
metricCh := make(chan Metric, 100)
|
||||
|
||||
// Producer: generate metrics
|
||||
go func() {
|
||||
defer close(metricCh)
|
||||
metrics := []string{"cpu", "memory", "disk", "network"}
|
||||
for i := 0; i < 100; i++ {
|
||||
metricCh <- Metric{
|
||||
MetricName: metrics[i%len(metrics)],
|
||||
Value: float64(i),
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Upsert from channel
|
||||
err = db.Upsert(
|
||||
"metrics",
|
||||
[]string{"metric_name"},
|
||||
[]string{"value", "timestamp"},
|
||||
"",
|
||||
metricCh,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Channel upsert failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("✓ Streamed 100 metric upserts")
|
||||
|
||||
// Verify
|
||||
var count int
|
||||
count, err = db.Count("SELECT COUNT(*) FROM `metrics`", 0)
|
||||
if err != nil {
|
||||
log.Printf("Count failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Total unique metrics: %d\n", count)
|
||||
}
|
||||
|
||||
// UpsertOrIgnoreExample demonstrates choosing between update and ignore
|
||||
func UpsertOrIgnoreExample() {
|
||||
db, err := setupDatabase()
|
||||
if err != nil {
|
||||
log.Printf("Setup failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("\nUpsert vs Insert Ignore Example")
|
||||
|
||||
type UniqueEmail struct {
|
||||
Email string `mysql:"email"`
|
||||
Count int `mysql:"count"`
|
||||
}
|
||||
|
||||
// With UPSERT - updates on duplicate
|
||||
email1 := UniqueEmail{Email: "test1@example.com", Count: 1}
|
||||
err = db.Upsert(
|
||||
"email_counts",
|
||||
[]string{"email"},
|
||||
[]string{"count"},
|
||||
"",
|
||||
email1,
|
||||
)
|
||||
fmt.Println(" Upsert: Inserts or updates")
|
||||
|
||||
// With INSERT IGNORE - silently ignores duplicate
|
||||
err = db.Exec(
|
||||
"INSERT IGNORE INTO `email_counts` (email, count) VALUES (@@email, @@count)",
|
||||
mysql.Params{"email": "test1@example.com", "count": 999})
|
||||
|
||||
fmt.Println(" Insert Ignore: Keeps original value on duplicate")
|
||||
|
||||
// Verify - count should still be 1 (ignore worked)
|
||||
var result UniqueEmail
|
||||
err = db.Select(&result,
|
||||
"SELECT `email`, `count` FROM `email_counts` WHERE `email` = @@email",
|
||||
0,
|
||||
"test1@example.com")
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Verification failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Count=%d (INSERT IGNORE kept original)\n", result.Count)
|
||||
}
|
||||
Reference in New Issue
Block a user