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", "admin@example.com", "hashed_password_here"},
{"user1", "user1@example.com", "hashed_password_here"},
{"user2", "user2@example.com", "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 feedback@compilenrun.com. I’d greatly appreciate your feedback! :)