Skip to main content

Echo Database Seeding

Database seeding is the process of populating a database with initial data when an application is first set up or when resetting it to a known state. In Echo applications, database seeding is essential for providing consistent test environments, demonstrating application features, and ensuring required data exists when your application launches.

Why Database Seeding Matters

Database seeding serves several important purposes:

  • Development consistency: Ensures all developers work with the same base data
  • Testing: Provides known data for automated tests
  • Demonstration: Prepares sample data for showcasing application features
  • Default data: Sets up required records like default user accounts, categories, or settings

Basic Seeding Concepts

Before diving into implementation, let's understand the core components of database seeding:

  1. Seed data: The predefined data to be inserted into the database
  2. Seeding mechanism: The code that handles the insertion
  3. Seed runners: The logic that determines when and how seeds are executed

Setting Up Seeding in an Echo Project

Let's create a simple seeding system for an Echo application with a SQL database.

Step 1: Create a Seed Structure

Start by organizing your seeding code in a dedicated folder:

go
// File: database/seeder/seeder.go
package seeder

import (
"database/sql"
"log"
)

// Seeder defines the interface for all seeders
type Seeder interface {
Run(db *sql.DB) error
}

// Execute runs all provided seeders
func Execute(db *sql.DB, seeders ...Seeder) error {
for _, seeder := range seeders {
if err := seeder.Run(db); err != nil {
return err
}
}
log.Println("Database seeding completed successfully")
return nil
}

Step 2: Create Individual Seeders

Now, create specific seeders for different types of data:

go
// File: database/seeder/users_seeder.go
package seeder

import (
"database/sql"
"log"
)

// UsersSeeder handles seeding user data
type UsersSeeder struct{}

// Run executes the seeder
func (s *UsersSeeder) Run(db *sql.DB) error {
log.Println("Seeding users...")

users := []struct {
Username string
Email string
Password string
}{
{"admin", "[email protected]", "hashed_password_here"},
{"user1", "[email protected]", "hashed_password_here"},
{"user2", "[email protected]", "hashed_password_here"},
}

stmt, err := db.Prepare("INSERT INTO users(username, email, password) VALUES(?, ?, ?)")
if err != nil {
return err
}
defer stmt.Close()

for _, user := range users {
_, err := stmt.Exec(user.Username, user.Email, user.Password)
if err != nil {
return err
}
}

log.Println("Users seeded successfully")
return nil
}

Let's create another seeder for product data:

go
// File: database/seeder/products_seeder.go
package seeder

import (
"database/sql"
"log"
)

// ProductsSeeder handles seeding product data
type ProductsSeeder struct{}

// Run executes the seeder
func (s *ProductsSeeder) Run(db *sql.DB) error {
log.Println("Seeding products...")

products := []struct {
Name string
Description string
Price float64
Stock int
}{
{"Keyboard", "Mechanical gaming keyboard", 89.99, 50},
{"Mouse", "Wireless optical mouse", 49.99, 100},
{"Monitor", "27-inch 4K display", 299.99, 25},
{"Headphones", "Noise-cancelling headphones", 129.99, 75},
}

stmt, err := db.Prepare("INSERT INTO products(name, description, price, stock) VALUES(?, ?, ?, ?)")
if err != nil {
return err
}
defer stmt.Close()

for _, product := range products {
_, err := stmt.Exec(product.Name, product.Description, product.Price, product.Stock)
if err != nil {
return err
}
}

log.Println("Products seeded successfully")
return nil
}

Step 3: Create a Database Manager

Now, let's create a database manager that initializes the database schema and runs the seeders:

go
// File: database/database.go
package database

import (
"database/sql"
"log"

"yourproject/database/seeder"
)

// Initialize sets up the database and runs migrations and seeders
func Initialize(db *sql.DB) error {
if err := createTables(db); err != nil {
return err
}

if err := seedData(db); err != nil {
return err
}

return nil
}

// createTables creates all necessary tables
func createTables(db *sql.DB) error {
// Create users table
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
if err != nil {
return err
}

// Create products table
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
price REAL NOT NULL,
stock INTEGER NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
if err != nil {
return err
}

log.Println("Database tables created successfully")
return nil
}

// seedData populates the database with initial data
func seedData(db *sql.DB) error {
return seeder.Execute(
db,
&seeder.UsersSeeder{},
&seeder.ProductsSeeder{},
)
}

Step 4: Integrating with Echo

Now, let's integrate our database initialization and seeding with an Echo application:

go
// File: main.go
package main

import (
"database/sql"
"log"

"github.com/labstack/echo/v4"
_ "github.com/mattn/go-sqlite3"

"yourproject/database"
)

func main() {
// Initialize database
db, err := sql.Open("sqlite3", "./database.db")
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
defer db.Close()

// Initialize schema and seed data
if err := database.Initialize(db); err != nil {
log.Fatalf("Failed to initialize database: %v", err)
}

// Create Echo instance
e := echo.New()

// Routes and other middleware...

// Start server
e.Logger.Fatal(e.Start(":8080"))
}

Advanced Seeding Techniques

Let's explore some more advanced seeding techniques:

Conditional Seeding

Sometimes you want to seed only under certain conditions:

go
// File: database/database.go (modified)
package database

import (
"database/sql"
"log"
"os"

"yourproject/database/seeder"
)

// Initialize sets up the database and runs migrations and seeders
func Initialize(db *sql.DB) error {
if err := createTables(db); err != nil {
return err
}

// Seed only in development or test environments
env := os.Getenv("APP_ENV")
if env == "development" || env == "testing" {
if err := seedData(db); err != nil {
return err
}
}

return nil
}

// ... rest of the file remains the same

Relationships in Seed Data

Handling relationships between tables requires careful ordering:

go
// File: database/seeder/orders_seeder.go
package seeder

import (
"database/sql"
"log"
"time"
)

// OrdersSeeder handles seeding order data with related products
type OrdersSeeder struct{}

// Run executes the seeder
func (s *OrdersSeeder) Run(db *sql.DB) error {
log.Println("Seeding orders...")

// First, get user IDs from the database
var userIDs []int
rows, err := db.Query("SELECT id FROM users")
if err != nil {
return err
}
defer rows.Close()

for rows.Next() {
var id int
if err := rows.Scan(&id); err != nil {
return err
}
userIDs = append(userIDs, id)
}

// Get product IDs from the database
var productIDs []int
rows, err = db.Query("SELECT id FROM products")
if err != nil {
return err
}
defer rows.Close()

for rows.Next() {
var id int
if err := rows.Scan(&id); err != nil {
return err
}
productIDs = append(productIDs, id)
}

// Create orders
for i, userID := range userIDs {
// Skip if we don't have enough users or products
if i >= len(productIDs) {
break
}

// Create order
orderStmt, err := db.Prepare("INSERT INTO orders(user_id, total_amount, status, created_at) VALUES(?, ?, ?, ?)")
if err != nil {
return err
}

result, err := orderStmt.Exec(userID, 99.99 * float64(i+1), "completed", time.Now())
if err != nil {
return err
}

orderID, err := result.LastInsertId()
if err != nil {
return err
}

// Create order items
itemStmt, err := db.Prepare("INSERT INTO order_items(order_id, product_id, quantity, price) VALUES(?, ?, ?, ?)")
if err != nil {
return err
}

_, err = itemStmt.Exec(orderID, productIDs[i], i+1, 99.99)
if err != nil {
return err
}

orderStmt.Close()
itemStmt.Close()
}

log.Println("Orders seeded successfully")
return nil
}

Seeding Large Datasets

For larger datasets, you might want to use transactions for better performance:

go
// File: database/seeder/large_dataset_seeder.go
package seeder

import (
"database/sql"
"log"
"math/rand"
"time"
)

// LargeDatasetSeeder seeds a large amount of data for performance testing
type LargeDatasetSeeder struct {
Count int // Number of records to create
}

// Run executes the seeder
func (s *LargeDatasetSeeder) Run(db *sql.DB) error {
log.Printf("Seeding large dataset (%d records)...", s.Count)

// Begin transaction for better performance
tx, err := db.Begin()
if err != nil {
return err
}

// Prepare statement within transaction
stmt, err := tx.Prepare("INSERT INTO analytics_data(user_id, page_viewed, view_time, duration) VALUES(?, ?, ?, ?)")
if err != nil {
tx.Rollback()
return err
}
defer stmt.Close()

// Get user IDs for reference
var userIDs []int
rows, err := db.Query("SELECT id FROM users")
if err != nil {
tx.Rollback()
return err
}

for rows.Next() {
var id int
if err := rows.Scan(&id); err != nil {
rows.Close()
tx.Rollback()
return err
}
userIDs = append(userIDs, id)
}
rows.Close()

// Seed random data
rand.Seed(time.Now().UnixNano())
pages := []string{"/home", "/products", "/contact", "/about", "/signup", "/login"}

for i := 0; i < s.Count; i++ {
userID := userIDs[rand.Intn(len(userIDs))]
page := pages[rand.Intn(len(pages))]
viewTime := time.Now().AddDate(0, 0, -rand.Intn(30))
duration := rand.Intn(300) // 0-300 seconds

_, err := stmt.Exec(userID, page, viewTime, duration)
if err != nil {
tx.Rollback()
return err
}

// Log progress periodically
if i > 0 && i%5000 == 0 {
log.Printf("Seeded %d records...", i)
}
}

// Commit transaction
if err := tx.Commit(); err != nil {
return err
}

log.Printf("Large dataset seeded successfully (%d records)", s.Count)
return nil
}

Real-World Example: E-commerce Application

Let's see how seeding would work in a complete e-commerce application:

go
// File: database/seeder/runner.go
package seeder

import (
"database/sql"
"log"
)

// RunAllSeeders executes all seeders in the correct order
func RunAllSeeders(db *sql.DB) error {
log.Println("Starting database seeding...")

// Define seeders in order of dependencies
seeders := []Seeder{
&UsersSeeder{},
&CategoriesSeeder{},
&ProductsSeeder{},
&ReviewsSeeder{},
&OrdersSeeder{},
&PaymentsSeeder{},
}

// Run them all
if err := Execute(db, seeders...); err != nil {
return err
}

// Only run large dataset seeder in testing environment
if isDevelopment() {
if err := Execute(db, &LargeDatasetSeeder{Count: 100}); err != nil {
return err
}
} else if isTesting() {
if err := Execute(db, &LargeDatasetSeeder{Count: 10000}); err != nil {
return err
}
}

log.Println("All seeders completed successfully")
return nil
}

func isDevelopment() bool {
// Implementation to check if we're in development environment
return true
}

func isTesting() bool {
// Implementation to check if we're in testing environment
return false
}

Then in your main.go:

go
// File: main.go
package main

import (
"database/sql"
"log"

"github.com/labstack/echo/v4"
_ "github.com/mattn/go-sqlite3"

"yourproject/database"
"yourproject/database/seeder"
)

func main() {
// Initialize database
db, err := sql.Open("sqlite3", "./database.db")
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
defer db.Close()

// Create tables
if err := database.CreateTables(db); err != nil {
log.Fatalf("Failed to create tables: %v", err)
}

// Seed database
if err := seeder.RunAllSeeders(db); err != nil {
log.Fatalf("Failed to seed database: %v", err)
}

// Create Echo instance
e := echo.New()

// Define routes, middleware, etc.

// Start server
e.Logger.Fatal(e.Start(":8080"))
}

Best Practices for Database Seeding

  1. Idempotency: Seeders should be designed to run multiple times without creating duplicate data
  2. Environment awareness: Different environments may need different seeding strategies
  3. Order matters: Seed dependent tables in the correct order to maintain referential integrity
  4. Performance: Use transactions for large datasets to improve seeding speed
  5. Realistic data: Use realistic examples that represent actual usage
  6. Version control: Keep seeders in your version control system to ensure all developers use the same data

Summary

Database seeding is a crucial part of modern application development, ensuring consistent data across development environments, tests, and even production systems. In Echo applications, implementing a robust seeding system helps to:

  • Accelerate development with pre-populated data
  • Ensure consistent testing environments
  • Showcase application features with demo data
  • Initialize required default values

By following the structured approach outlined in this guide, you can implement an efficient, maintainable seeding system for your Echo applications.

Additional Resources

Exercises

  1. Create a seeder for a blog application that seeds users, categories, posts, and comments.
  2. Modify the existing seeders to check if data already exists before inserting.
  3. Create a CLI command that allows running specific seeders on demand.
  4. Implement a seeder that loads data from JSON files instead of hardcoding it in the Go files.
  5. Create a seeder that generates 1000 random products with different categories and prices.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)