> Full Neon documentation index: https://neon.com/docs/llms.txt

# Using GORM with Lakebase Postgres

Learn how to use GORM, Go's most popular ORM, with Lakebase Postgres on Neon

[GORM](https://gorm.io/) is Go's most popular ORM library, providing a developer-friendly interface to interact with databases. Paired with a Postgres database on Neon, it gives you a Go data layer without a database server to manage.

This guide walks through integrating GORM with Lakebase Postgres, from connecting and defining models to migrations and performance tips.

## Prerequisites

Before getting started, make sure you have:

- [Go](https://golang.org/dl/) 1.18 or later installed
- A [Neon](https://console.neon.tech/signup) account
- Basic familiarity with Go and SQL

## Set up your environment

### Create a Neon project

If you don't have one already, create a Neon project:

1. Navigate to the [Projects page](https://console.neon.tech/app/projects) in the Neon Console
2. Click **New Project**
3. Specify your project settings and click **Create project**

Save your connection details including your password. You'll need these when configuring your Go application.

### Initialize your Go project

Start by setting up your project structure. In Go, projects are organized as modules, which manage dependencies and package versioning. A module is initialized with a unique module path that distinguishes your project in the Go ecosystem.

Create a new directory for your project and initialize a Go module:

```bash
mkdir neon-gorm-example
cd neon-gorm-example
go mod init example.com/neon-gorm
```

This creates a `go.mod` file that will track your project's dependencies. The `example.com/neon-gorm` is the module path and should be replaced with your own domain or GitHub repository if you plan to publish your code.

### Install required packages

This guide uses two packages:

1. **GORM** - The ORM library that provides a developer-friendly interface to interact with the database
2. **GORM Postgres driver** - The database driver that allows GORM to connect to Postgres databases

Run the following commands to install these packages:

```bash
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
```

These commands fetch the latest versions of the packages and add them to your project's `go.mod` file. The `-u` flag ensures you get the most recent version of each package.

## Connect to Neon with GORM

### Basic connection setup

Next, connect to your database on Neon using GORM. The rest of the guide uses this connection.

Create a new file named `main.go` with the following code:

```go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/postgres"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

func main() {
	// Connection string for Neon Postgres
	dsn := "postgresql://[user]:[password]@[neon_hostname]/[dbname]?sslmode=require&channel_binding=require"

	// Connect to the database
	db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Info), // Set to Info level for development
	})
	if err != nil {
		log.Fatalf("Failed to connect to database: %v", err)
	}

	// Get the underlying SQL DB object
	sqlDB, err := db.DB()
	if err != nil {
		log.Fatalf("Failed to get DB object: %v", err)
	}

	// Verify connection
	if err := sqlDB.Ping(); err != nil {
		log.Fatalf("Failed to ping DB: %v", err)
	}

	fmt.Println("Successfully connected to Neon Postgres database!")
}
```

This code:

1. Defines a DSN, the connection string that contains all the information needed to connect to your Neon database
2. Uses `gorm.Open()` to establish a connection with the Postgres driver
3. Configures GORM's logger to show SQL queries during development, which helps with debugging
4. Gets the underlying `*sql.DB` object to access lower-level database functions
5. Verifies the connection is active by pinging the database

Replace `[user]`, `[password]`, `[neon_hostname]`, and `[dbname]` with your actual Neon connection details. You can find these by clicking **Connect** in the Console nav. The `?sslmode=require&channel_binding=require` part of the connection string enforces an encrypted connection to your database.

### Connection pooling and configuration

Connection pooling maintains a set of reusable database connections, which avoids the overhead of opening a new connection for each operation.

#### Neon connection pooling

Neon provides a **built-in connection pooler**, powered by PgBouncer, that accepts up to 10,000 client connections per compute while reusing a smaller number of Postgres connections.

Instead of each request opening a new database connection, the pooler distributes queries across existing backend connections. To use it, use the pooled connection string, which has `-pooler` in the hostname. In the Neon Console, click **Connect** and turn on the **Connection pooling** toggle to copy it.

Neon's pooler runs in **transaction pooling mode**, so session-based features like `LISTEN/NOTIFY`, `SET search_path`, and SQL-level `PREPARE`/`EXECUTE` aren't supported on pooled connections. Protocol-level prepared statements, which most drivers use, are supported. For operations that require session state, use a direct (non-pooled) connection. For details, see [Connection pooling](https://neon.com/docs/connect/connection-pooling).

#### Configure connection pooling in GORM

GORM uses the connection pool from Go's `database/sql`, which works alongside Neon's pooler. Settings like `SetMaxOpenConns` and `SetConnMaxIdleTime` control how your application manages connections before they reach the database.

Because Neon's pooler already handles pooling on the database side, keep a moderate number of open connections in your application to avoid excessive connection churn.

The recommended approach is to use a pooled connection string for normal queries and switch to a direct connection for migration tasks that require session state. For guidance on configuring connection pooling in Go, refer to the [GORM connection documentation](https://gorm.io/docs/generic_interface.html#Connection-Pool).

## Define models

In GORM, models are Go structs that represent tables in your database. Each field in the struct maps to a column in the table, and GORM uses struct tags (annotations enclosed in backticks) to configure how these fields are handled in the database.

Here's a simple blogging application with `User` and `Post` models. These models define the structure of the database tables and the relationships between them.

```go
package main

import (
	"time"

	"gorm.io/gorm"
)

// User represents a user in the system
type User struct {
	ID        uint           `gorm:"primaryKey"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt `gorm:"index"`
	Name      string         `gorm:"size:255;not null"`
	Email     string         `gorm:"size:255;not null;uniqueIndex"`
	Posts     []Post         `gorm:"foreignKey:UserID"`
}

// Post represents a blog post
type Post struct {
	ID        uint           `gorm:"primaryKey"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt `gorm:"index"`
	Title     string         `gorm:"size:255;not null"`
	Content   string         `gorm:"type:text"`
	UserID    uint           `gorm:"not null"`
	User      User           `gorm:"foreignKey:UserID"`
}
```

The key components of these models:

1. **Basic fields**: `ID`, `CreatedAt`, `UpdatedAt`, and `DeletedAt` are standard fields in GORM models. They handle primary keys, timestamps, and soft deletion.

2. **Field tags**: The struct tags like `gorm:"size:255;not null"` define constraints and properties for each field:
   - `primaryKey`: Designates a field as the table's primary key
   - `size:255`: Sets the column's maximum length
   - `not null`: Ensures the field cannot be empty
   - `uniqueIndex`: Creates a unique index on the column
   - `type:text`: Specifies the SQL data type

3. **Relationships**: The `Posts` field in the User model and the `User` field in the Post model establish a one-to-many relationship. The `foreignKey` tag specifies which field serves as the foreign key.

By default, GORM pluralizes struct names to create table names (for example, `User` becomes `users`) and converts field names to snake_case column names. You can customize table names with a `TableName` method and column names with the `gorm:"column:custom_name"` tag. This is similar to how other ORMs like Sequelize and Eloquent work.

## Automatic migrations

Migrations are a way to manage database schema changes over time. GORM provides a convenient `AutoMigrate` feature that automatically creates tables, indexes, constraints, and foreign keys based on your model definitions.

This automation is useful during development, but for production environments you'll typically want more control over schema changes. We'll cover structured migrations for production later in this guide, but first, here's how to use GORM's automatic migrations for development.

Here's how to set up automatic migrations:

```go
func main() {
	// Connection setup (as shown above)

	// Auto-migrate the schema
	err = db.AutoMigrate(&User{}, &Post{})
	if err != nil {
		log.Fatalf("Failed to migrate database: %v", err)
	}

	fmt.Println("Database migrated successfully!")
}
```

When you run this code, GORM will:

1. Check if the tables exist, and create them if they don't
2. Add missing columns to existing tables
3. Create indexes and constraints
4. Establish foreign key relationships between tables

The `AutoMigrate` function works by comparing your Go struct definitions to the actual database schema and making necessary changes to align them. It accepts a list of model struct pointers and returns an error if something goes wrong.

`AutoMigrate` only adds things that are missing. It won't delete columns or tables that exist in the database but not in your models, which prevents accidental data loss.

## Basic CRUD operations

With the models and database connection set up, you can perform basic Create, Read, Update, and Delete (CRUD) operations.

### Create records

GORM creates new records with the `Create` method. This code adds a user and a blog post:

```go
// Create a new user
user := User{
	Name:  "John Doe",
	Email: "john@example.com",
}
result := db.Create(&user)
if result.Error != nil {
	log.Fatalf("Failed to create user: %v", result.Error)
}
fmt.Printf("Created user with ID: %d\n", user.ID)

// Create a post for the user
post := Post{
	Title:   "Getting Started with GORM and Neon",
	Content: "GORM makes it easy to work with databases in Go...",
	UserID:  user.ID,
}
result = db.Create(&post)
if result.Error != nil {
	log.Fatalf("Failed to create post: %v", result.Error)
}
fmt.Printf("Created post with ID: %d\n", post.ID)
```

Here's what happens in this code:

1. We create a new `User` struct instance with basic information
2. We pass a pointer to this struct to the `db.Create()` method, which inserts it into the database
3. GORM automatically handles generating the primary key ID, timestamps, and other default values
4. After successful creation, the user's generated ID is populated in the `user.ID` field
5. We then create a `Post` struct, setting the `UserID` field to establish the relationship with our user
6. We insert the post into the database using the same `Create` method

GORM returns a `result` object that contains an `Error` field. Always check this field to ensure your database operations succeeded. The result object also provides other useful information like the number of rows affected by the operation.

### Read records

GORM provides several methods for retrieving data, from simple lookups to complex queries:

```go
// Retrieve a user by ID
var retrievedUser User
result = db.First(&retrievedUser, user.ID)
if result.Error != nil {
	log.Fatalf("Failed to retrieve user: %v", result.Error)
}
fmt.Printf("Retrieved user: %s (%s)\n", retrievedUser.Name, retrievedUser.Email)

// Retrieve a user with their posts
var userWithPosts User
result = db.Preload("Posts").First(&userWithPosts, user.ID)
if result.Error != nil {
	log.Fatalf("Failed to retrieve user with posts: %v", result.Error)
}
fmt.Printf("User %s has %d posts\n", userWithPosts.Name, len(userWithPosts.Posts))

// Find users with specific criteria
var users []User
result = db.Where("name LIKE ?", "%John%").Find(&users)
if result.Error != nil {
	log.Fatalf("Failed to find users: %v", result.Error)
}
fmt.Printf("Found %d users with 'John' in their name\n", len(users))
```

These queries use:

1. **Simple retrieval**: The `First` method retrieves the first record that matches the condition. In our first example, we're finding a user by their ID, which should return exactly one record since IDs are unique.

2. **Eager loading with Preload**: The `Preload` method loads related records along with the parent. In the second example, you load a user and all their posts in one call. GORM runs one extra query for the association instead of a separate query per post.

3. **Conditional queries with Where**: The `Where` method specifies conditions for a query. The third example uses the SQL `LIKE` operator to find users whose names contain "John". The `?` is a placeholder that helps prevent SQL injection attacks.

GORM provides many other query methods that we haven't covered here, such as:

- `Last`: Retrieves the last record matching the condition
- `Take`: Retrieves a record without any specified order
- `Pluck`: Retrieves a single column from the database as a slice
- `Count`: Returns the number of records matching the condition

All these methods return a result object that contains an `Error` field, which should be checked to ensure the query was successful.

### Update records

GORM can update a single field, multiple fields, or run more complex update operations:

```go
// Update a user's email
result = db.Model(&user).Update("email", "johndoe@example.com")
if result.Error != nil {
	log.Fatalf("Failed to update user: %v", result.Error)
}

// Update multiple fields at once
result = db.Model(&post).Updates(Post{
	Title:   "Updated: Getting Started with GORM and Neon",
	Content: "Updated content about GORM and Neon...",
})
if result.Error != nil {
	log.Fatalf("Failed to update post: %v", result.Error)
}
```

These update operations work as follows:

1. **Single field update**: The `Update` method changes a single column's value. The first example updates the user's email address. We use the `Model` method to specify which record to update (based on its primary key).

2. **Multiple field update**: The `Updates` method changes multiple columns at once. We provide a struct with the fields we want to update. Note that GORM will only update non-zero fields by default, which means fields with their zero values (empty string, 0, false, etc.) won't be updated unless you use `Updates` with a map.

GORM also offers other update methods:

- **Update with conditions**: `db.Model(&User{}).Where("name = ?", "john").Update("name", "jane")`
- **Batch updates**: `db.Table("users").Where("role = ?", "admin").Update("active", true)`
- **Raw SQL updates**: `db.Exec("UPDATE users SET name = ? WHERE age > ?", "Jane", 20)`

When updating records, GORM automatically sets the `UpdatedAt` field to the current time if your model includes this field.

### Delete records

GORM provides two types of deletion: soft deletion and hard deletion. Soft deletion marks records as deleted without actually removing them from the database, while hard deletion permanently removes records.

Here's how to perform both:

```go
// Soft delete a post (with GORM's DeletedAt field)
result = db.Delete(&post)
if result.Error != nil {
	log.Fatalf("Failed to delete post: %v", result.Error)
}

// Hard delete a post (permanently remove from database)
result = db.Unscoped().Delete(&post)
if result.Error != nil {
	log.Fatalf("Failed to permanently delete post: %v", result.Error)
}
```

These deletion operations work as follows:

1. **Soft deletion**: When we call `Delete` on a model that has a `DeletedAt` field (like our models do thanks to `gorm.Model`), GORM performs a soft delete. This doesn't actually remove the record from the database; instead, it sets the `DeletedAt` field to the current time. Subsequent queries will automatically exclude these "deleted" records unless you explicitly include them.

2. **Hard deletion**: The `Unscoped` method tells GORM to ignore the soft delete mechanism and permanently remove the record from the database.

Soft deletion is useful for:

- Keeping an audit trail of records
- Allowing data to be restored if deleted accidentally
- Maintaining referential integrity in related data
- Meeting regulatory requirements that prohibit true deletion

By default, GORM queries won't return soft-deleted records. If you need to include them, you can use the `Unscoped` method: `db.Unscoped().Where("name = ?", "John").Find(&users)`.

## Advanced GORM features

### Transactions

Transactions group multiple database operations into a single unit of work, so either all of them succeed or none do. GORM supports database transactions:

```go
// Begin a transaction
tx := db.Begin()

// Perform operations within the transaction
user := User{Name: "Transaction User", Email: "tx@example.com"}
if err := tx.Create(&user).Error; err != nil {
	tx.Rollback() // Rollback if there's an error
	log.Fatalf("Failed to create user in transaction: %v", err)
}

post := Post{
	Title:   "Post in Transaction",
	Content: "This post is created in a transaction",
	UserID:  user.ID,
}
if err := tx.Create(&post).Error; err != nil {
	tx.Rollback() // Rollback if there's an error
	log.Fatalf("Failed to create post in transaction: %v", err)
}

// Commit the transaction
if err := tx.Commit().Error; err != nil {
	log.Fatalf("Failed to commit transaction: %v", err)
}
```

Here's how transactions work in GORM:

1. **Begin a transaction**: The `Begin` method starts a new transaction and returns a transaction object.
2. **Perform operations**: Use the transaction object (instead of the regular db object) to perform database operations. All operations will be part of the transaction.
3. **Roll back or commit**: If any operation fails, call `Rollback` to cancel all changes. If all operations succeed, call `Commit` to permanently apply the changes to the database.

Use transactions in scenarios like:

- Creating a user and their profile simultaneously
- Transferring funds between accounts
- Processing an order with multiple line items

For a more concise approach, GORM provides a transaction helper method that automatically handles the begin, commit, and rollback operations:

```go
err := db.Transaction(func(tx *gorm.DB) error {
	user := User{Name: "Transaction User", Email: "tx@example.com"}
	if err := tx.Create(&user).Error; err != nil {
		return err
	}

	post := Post{
		Title:   "Post in Transaction",
		Content: "This post is created in a transaction",
		UserID:  user.ID,
	}
	if err := tx.Create(&post).Error; err != nil {
		return err
	}

	return nil
})

if err != nil {
	log.Fatalf("Transaction failed: %v", err)
}
```

This approach is generally preferred because:

1. **Simpler error handling**: You return an error from the closure function, and GORM automatically rolls back the transaction if an error is returned.
2. **Cleaner code**: The transaction logic is encapsulated in a single function, making the code more readable.
3. **Automatic resource management**: GORM ensures that the transaction is properly closed whether it succeeds or fails, preventing resource leaks.

With this method, you focus on the business logic inside the transaction rather than managing the transaction lifecycle. If the function returns nil, the transaction is committed; if it returns an error, the transaction is rolled back automatically.

### Raw SQL and complex queries

GORM's built-in query methods cover most common scenarios. When a query is easier to express in raw SQL, GORM still handles parameter binding and result mapping:

```go
// Execute raw SQL
var result []map[string]interface{}
db.Raw("SELECT u.name, COUNT(p.id) as post_count FROM users u LEFT JOIN posts p ON u.id = p.user_id GROUP BY u.name").Scan(&result)

// Combined with GORM methods
var users []User
db.Raw("SELECT * FROM users WHERE name = ?", "John").Scan(&users)

// Complex queries
var userStats []struct {
	UserName  string
	PostCount int
}
db.Table("users").
	Select("users.name as user_name, COUNT(posts.id) as post_count").
	Joins("left join posts on posts.user_id = users.id").
	Where("users.deleted_at IS NULL").
	Group("users.name").
	Having("COUNT(posts.id) > ?", 1).
	Find(&userStats)
```

These examples show three approaches:

1. **Raw SQL with generic results**: The first example executes a raw SQL query and scans the results into a slice of maps. This is useful when you don't have a predefined struct for the result or need flexibility in handling different result shapes.

2. **Raw SQL with model mapping**: The second example shows how you can execute raw SQL but still map the results to your model structs. GORM handles the mapping between column names and struct fields.

3. **Query builder API**: The third example demonstrates GORM's query builder API, which provides a fluent interface for constructing complex queries. This approach offers:
   - Type safety and IDE auto-completion
   - SQL injection protection with parameter placeholders
   - Readability for complex queries
   - The ability to build queries dynamically based on conditions

When should you use raw SQL versus GORM's query builder?

- **Use raw SQL when**: You have complex queries that are difficult to express with the query builder, or when you're optimizing performance with database-specific features.
- **Use the query builder when**: You want type safety, need to build queries dynamically, or prefer a more Go-idiomatic approach.

In either case, GORM handles parameter binding to protect against SQL injection, so both approaches are safe when used correctly.

### Hooks

Hooks (also known as callbacks) are functions that are called at specific stages of the database operation lifecycle. They allow you to inject custom logic before or after these operations, such as validation, data transformation, or triggering side effects.

GORM provides hooks for various operations. You can find the full list of hooks in the [GORM documentation](https://gorm.io/docs/hooks.html). Here are a couple of common hooks:

```go
// Define hooks in your model
type User struct {
	// ... fields as defined earlier
	Password string `gorm:"size:255;not null"`
}

// BeforeCreate is called before a record is created
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
	// For demonstration purposes - in real apps use proper password hashing!
	u.Password = "hashed_" + u.Password
	return
}

// AfterFind is called after a record is retrieved
func (u *User) AfterFind(tx *gorm.DB) (err error) {
	// Custom logic after a user is found
	return
}
```

Hooks keep business logic in one place instead of duplicating it across your application. Because they're methods on your model structs, the behavior lives with the data it operates on.

## Structured migrations for production

`AutoMigrate` is convenient for development, but production systems need precise control over when and how schema changes happen, with the ability to roll them back.

[Golang-migrate](https://github.com/golang-migrate/migrate) is a popular migration tool for Go applications that provides version-controlled, reversible database migrations. To set up migrations with it:

1. Install the migrate CLI:

   ```bash
   go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
   ```

   This installs the migration tool globally on your system, allowing you to run migrations from the command line.

2. Create the `migrations` directory:

   ```bash
   mkdir -p migrations
   ```

   This directory will store all your migration files in a structured, version-controlled format.

3. Create your first migration:

   ```bash
   migrate create -ext sql -dir migrations -seq create_users_table
   ```

   This creates two files: an "up" migration for applying changes and a "down" migration for reverting them. The `-seq` flag ensures migrations are numbered sequentially, which helps maintain the order of execution.

4. Edit the up migration (`migrations/000001_create_users_table.up.sql`):

   ```sql
   CREATE TABLE users (
       id SERIAL PRIMARY KEY,
       created_at TIMESTAMP NOT NULL DEFAULT NOW(),
       updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
       deleted_at TIMESTAMP,
       name VARCHAR(255) NOT NULL,
       email VARCHAR(255) NOT NULL UNIQUE
   );

   CREATE INDEX idx_users_deleted_at ON users(deleted_at);
   ```

   The "up" migration contains SQL to create new database objects or modify existing ones. Here, we're creating a users table with the necessary columns and an index.

5. Edit the down migration (`migrations/000001_create_users_table.down.sql`):

   ```sql
   DROP TABLE IF EXISTS users;
   ```

   The "down" migration contains SQL to undo the changes made by the corresponding "up" migration. This ensures you can roll back changes if needed. In this case, we're dropping the users table.

6. Create a migration for the posts table:

   ```bash
   migrate create -ext sql -dir migrations -seq create_posts_table
   ```

   This second migration creates the posts table, which depends on the users table created in the first migration.

7. Edit the up migration (`migrations/000002_create_posts_table.up.sql`):

   ```sql
   CREATE TABLE posts (
       id SERIAL PRIMARY KEY,
       created_at TIMESTAMP NOT NULL DEFAULT NOW(),
       updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
       deleted_at TIMESTAMP,
       title VARCHAR(255) NOT NULL,
       content TEXT,
       user_id INTEGER NOT NULL REFERENCES users(id)
   );

   CREATE INDEX idx_posts_deleted_at ON posts(deleted_at);
   ```

   Note the foreign key reference to the users table (`REFERENCES users(id)`). This ensures referential integrity between the two tables.

8. Edit the down migration (`migrations/000002_create_posts_table.down.sql`):

   ```sql
   DROP TABLE IF EXISTS posts;
   ```

   Again, the down migration drops the table to reverse the changes.

9. Run the migrations:

   ```bash
   export POSTGRESQL_URL="postgresql://[user]:[password]@[neon_hostname]/[dbname]?sslmode=require&channel_binding=require"
   migrate -database ${POSTGRESQL_URL} -path migrations up
   ```

   This command applies all pending migrations to your database. The `up` argument tells the tool to apply migrations that haven't been applied yet. You can also use `down` to reverse migrations, or specify a version number to migrate to a specific version.

The migrate tool keeps track of which migrations have been applied in a special table called `schema_migrations` in your database. This ensures that migrations are only applied once and in the correct order.

For more complex applications, you can run migrations programmatically from Go. This lets you:

1. Run migrations as part of your application startup
2. Use the same code in different environments (development, staging, production)
3. Add custom logic around migrations, like waiting for the database to be ready

Here's how to create a Go function to run migrations programmatically:

```go
package main

import (
	"log"

	"github.com/golang-migrate/migrate/v4"
	_ "github.com/golang-migrate/migrate/v4/database/postgres"
	_ "github.com/golang-migrate/migrate/v4/source/file"
)

func runMigrations() {
	m, err := migrate.New(
		"file://migrations",
		"postgresql://[user]:[password]@[neon_hostname]/[dbname]?sslmode=require&channel_binding=require",
	)
	if err != nil {
		log.Fatalf("Failed to create migration instance: %v", err)
	}

	if err := m.Up(); err != nil && err != migrate.ErrNoChange {
		log.Fatalf("Failed to run migrations: %v", err)
	}

	log.Println("Migrations completed successfully")
}

func main() {
	runMigrations()

	// Continue with your application...
}
```

This function:

1. Creates a new migration instance that reads migration files from the local filesystem
2. Configures the database connection using your Neon credentials
3. Runs all pending migrations with `m.Up()`
4. Handles errors appropriately, distinguishing between actual errors and the "no changes" case

You would typically call this function early in your application's startup process, before initializing your GORM instance. This ensures that your database schema is up-to-date before your application starts using it.

For more advanced scenarios, you might want to add features like:

- Checking database connectivity before running migrations
- Adding retry logic for transient connection issues
- Implementing a "migrate and seed" function for development environments
- Adding version reporting to track which migrations have been applied

## Performance optimization with Neon

When working with Neon and GORM, consider these performance optimization techniques:

### Efficient querying

One of the most effective ways to improve performance is to retrieve only the data you need. Fetching only the specific fields you need reduces the amount of data transferred between Neon and your application, resulting in faster queries and less memory usage.

For example:

```go
var users []struct {
	ID    uint
	Name  string
	Email string
}
db.Model(&User{}).Select("id", "name", "email").Where("id > ?", 10).Find(&users)
```

In this example, instead of selecting all fields with `Find(&users)`, we're using the `Select` method to specify exactly which columns we want. This has several benefits:

1. **Less data transfer**: Only the specified columns are fetched, reducing network bandwidth usage.
2. **Better query performance**: The database can optimize the query better when it knows exactly which columns to return.
3. **Lower memory usage**: Your application only stores the data it actually needs.

Notice that we're also using an anonymous struct that contains only the fields we're interested in, rather than using the full `User` model. This avoids allocating memory for fields you don't need.

### Batch processing

When working with large datasets, processing all the data at once can lead to performance issues, including high memory usage and long-running queries. Instead, you can use batch processing to handle large datasets in smaller, more manageable chunks.

GORM provides the `FindInBatches` method to simplify this pattern:

```go
// Find in batches
db.Model(&User{}).Where("active = ?", true).FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error {
	for _, result := range results {
		// Process result...
	}
	return nil
})
```

Here's what this code does:

1. We start a query on the User model for active users
2. Instead of fetching all results at once, we use `FindInBatches` to retrieve them in batches of 100 records
3. For each batch, GORM calls the provided callback function with the batch results
4. Inside the callback, we process each result individually

Batch processing matters most for operations that might affect millions of records, such as data migrations, report generation, or bulk updates. It's also useful when you need to perform complex processing on each record that might be resource-intensive.

For more information on batch processing and other advanced querying techniques, refer to the [GORM documentation](https://gorm.io/docs/advanced_query.html#FindInBatches).

### Indexing

Indexes have a large effect on query performance. With GORM, you can define indexes in your models:

```go
type User struct {
	ID      uint   `gorm:"primaryKey"`
	Name    string `gorm:"index:idx_name_email,unique"`
	Email   string `gorm:"index:idx_name_email,unique"`
	Address string `gorm:"index"`
}
```

For more complex indexing requirements, use migrations as shown in the previous section.

For more information on indexing and optimizing database performance, see the [Postgres indexes tutorial](https://neon.com/postgresql/postgresql-indexes).

## Complete application example

Here's everything together in one application:

```go
package main

import (
	"fmt"
	"log"
	"time"

	"gorm.io/driver/postgres"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

// User model
type User struct {
	gorm.Model
	Name     string `gorm:"size:255;not null"`
	Email    string `gorm:"size:255;not null;uniqueIndex"`
	Password string `gorm:"size:255;not null"`
	Posts    []Post `gorm:"foreignKey:UserID"`
}

// BeforeCreate hook for User
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
	// Simulate password hashing
	u.Password = "hashed_" + u.Password
	return
}

// Post model
type Post struct {
	gorm.Model
	Title   string `gorm:"size:255;not null"`
	Content string `gorm:"type:text"`
	UserID  uint   `gorm:"not null"`
	User    User   `gorm:"foreignKey:UserID"`
}

func main() {
	// Connection string for Neon Postgres
	dsn := "postgresql://[user]:[password]@[neon_hostname]/[dbname]?sslmode=require&channel_binding=require"

	// Connect to the database
	db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Info),
	})
	if err != nil {
		log.Fatalf("Failed to connect to database: %v", err)
	}

	// Get the underlying SQL DB object
	sqlDB, err := db.DB()
	if err != nil {
		log.Fatalf("Failed to get DB object: %v", err)
	}

	// Configure connection pool
	sqlDB.SetMaxIdleConns(5)
	sqlDB.SetMaxOpenConns(10)
	sqlDB.SetConnMaxLifetime(time.Hour)
	sqlDB.SetConnMaxIdleTime(30 * time.Minute)

	// Auto-migrate the schema
	err = db.AutoMigrate(&User{}, &Post{})
	if err != nil {
		log.Fatalf("Failed to migrate database: %v", err)
	}

	// Create a new user
	user := User{
		Name:     "John Doe",
		Email:    "john@example.com",
		Password: "secret123",
	}

	result := db.Create(&user)
	if result.Error != nil {
		log.Fatalf("Failed to create user: %v", result.Error)
	}
	fmt.Printf("Created user with ID: %d\n", user.ID)

	// Create posts for the user
	posts := []Post{
		{Title: "First Post", Content: "Content of first post", UserID: user.ID},
		{Title: "Second Post", Content: "Content of second post", UserID: user.ID},
	}

	result = db.Create(&posts)
	if result.Error != nil {
		log.Fatalf("Failed to create posts: %v", result.Error)
	}

	// Retrieve user with posts
	var userWithPosts User
	result = db.Preload("Posts").First(&userWithPosts, user.ID)
	if result.Error != nil {
		log.Fatalf("Failed to retrieve user with posts: %v", result.Error)
	}

	fmt.Printf("Retrieved user: %s (%s)\n", userWithPosts.Name, userWithPosts.Email)
	fmt.Printf("User has %d posts:\n", len(userWithPosts.Posts))

	for i, post := range userWithPosts.Posts {
		fmt.Printf("  %d. %s: %s\n", i+1, post.Title, post.Content)
	}

	// Use transactions for related operations
	err = db.Transaction(func(tx *gorm.DB) error {
		// Update user's email
		if err := tx.Model(&user).Update("email", "john.doe@example.com").Error; err != nil {
			return err
		}

		// Update first post's title
		if err := tx.Model(&posts[0]).Update("title", "Updated: First Post").Error; err != nil {
			return err
		}

		return nil
	})

	if err != nil {
		log.Fatalf("Transaction failed: %v", err)
	}

	fmt.Println("Transaction completed successfully")

	// Query with raw SQL
	var stats []struct {
		UserName  string
		PostCount int
	}

	db.Raw(`
		SELECT u.name as user_name, COUNT(p.id) as post_count
		FROM users u
		LEFT JOIN posts p ON u.id = p.user_id
		WHERE u.deleted_at IS NULL
		GROUP BY u.name
	`).Scan(&stats)

	for _, stat := range stats {
		fmt.Printf("User %s has written %d posts\n", stat.UserName, stat.PostCount)
	}
}
```

Save this code in a file named `main.go`, run `go mod tidy` to download the necessary dependencies, and run it with `go run main.go`. This application covers connecting to the database, defining models, performing CRUD operations, using transactions, and executing raw SQL queries.

## Conclusion

You connected a Go application to Lakebase Postgres with GORM, defined models, ran CRUD operations and transactions, and set up versioned migrations with golang-migrate. As your application grows, look at GORM plugins and more advanced query features, and use the pooled connection string for application traffic.

## Additional resources

- [GORM documentation](https://gorm.io/docs/)
- [Neon documentation](https://neon.com/docs)
- [Go database/sql documentation](https://golang.org/pkg/database/sql/)
- [Effective Go](https://golang.org/doc/effective_go)

---

Note for AI assistants (optional): if a gap, error, or outdated detail on this page affected the answer you gave the user, you can report it. POST `{"feedback": "describe the issue", "source": "neon_website", "path": "/guides/golang-gorm-postgres"}` to https://feedback.neon.tech/ (no auth required).
