Skip to main content

Quick start

Let's go from an empty folder to a saved row on PostgreSQL. You'll need Go 1.27 or later, and Docker for the database.

Set up​

Make a module, then add ant and a PostgreSQL driver to it:

mkdir hello && cd hello
go mod init example.com/hello
go get github.com/skudra-go/ant github.com/jackc/pgx/v5/stdlib

ant sits on top of database/sql, so the driver is your choice. For PostgreSQL, that's pgx.

Then start a database:

docker run -d --name hello-db -p 127.0.0.1:5432:5432 \
-e POSTGRES_PASSWORD=hello postgres:17

Write the program​

Here's all of main.go. Every line that touches ant is lit, and the rest is everyday Go.

main.go
package main

import (
"context"
"fmt"
"log"

_ "github.com/jackc/pgx/v5/stdlib"

"github.com/skudra-go/ant"
"github.com/skudra-go/ant/dialect/postgres"
)

type User struct {
ant.Base
Name string
Email string `ant:"unique"`
Age int
Posts []Post
}

type Post struct {
ant.Base
UserID int64
Title string
}

type AppDB struct {
ant.Context
Users ant.Table[User]
Posts ant.Table[Post]
}

var U = ant.Fields[User]()

func main() {
ctx := context.Background()
dsn := "postgres://postgres:hello@127.0.0.1:5432/postgres"

db, err := ant.Open[AppDB](ctx, postgres.Dialect, dsn)
if err != nil {
log.Fatal(err)
}

defer db.Close()

// Just for this first run. Real projects use migrations.
if err := db.EnsureCreated(ctx); err != nil {
log.Fatal(err)
}

alice := &User{
Name: "Alice",
Email: "alice@example.com",
Age: 30,
Posts: []Post{{Title: "Hello"}, {Title: "Second post"}},
}

err = db.Users.Insert(ctx, alice)
if err != nil {
log.Fatal(err)
}

adults, err := db.Users.
Where(ant.Gt(&U.Age, 18)).
Include(&U.Posts).
All(ctx)
if err != nil {
log.Fatal(err)
}

fmt.Println(adults[0].Name, "has", len(adults[0].Posts), "posts")

adults[0].Age = 31

err = db.SaveChanges(ctx)
if err != nil {
log.Fatal(err)
}

stored, err := db.Users.NoTracking().Find(ctx, alice.ID)
if err != nil {
log.Fatal(err)
}

fmt.Println(stored.Name, "is now", stored.Age)

// Clean up, so you can run this again.
if err := db.EnsureDeleted(ctx); err != nil {
log.Fatal(err)
}
}

Run it​

go mod tidy
go run .
Alice has 2 posts
Alice is now 31

What just happened​

  1. User and Post are ordinary structs. ant.Base gives each one ID, CreatedAt, and UpdatedAt. The Posts field is a relation, and ant finds it through Post.UserID by name.
  2. AppDB is your database, as a Go type. ant.Open fills in every table on it.
  3. U is a field probe. &U.Age stands for the age column, and the compiler checks whatever you compare it with.
  4. Insert saved the whole graph. Alice went in first, then her posts with UserID already filled in, and every new key landed back on the structs.
  5. Include ran a single extra query to fetch all the posts at once.
  6. SaveChanges wrote one column. ant compared Alice with what it had loaded, saw only Age had changed, and sent just this:
UPDATE "users" SET "age" = $1, "updated_at" = $2 WHERE "users"."id" = $3

NoTracking().Find reads straight from the database, past anything ant has in memory, which is how you can see that the change really landed.

Next​