So what does Pop do exactly? Well, it wraps the absolutely amazing https://github.com/jmoiron/sqlx library. It cleans up some of the common patterns and work flows usually associated with dealing with databases in Go.
Pop makes it easy to do CRUD operations, run migrations, and build/execute queries. Is Pop an ORM? I'll leave that up to you, the reader, to decide.
Pop, by default, follows conventions that were influenced by the ActiveRecord Ruby gem. What does this mean?
- Tables must have an "id" column and a corresponding "ID" field on the
struct
being used. - If there is a
timestamp
column namedcreated_at
, and aCreatedAt time.Time
attribute on thestruct
, it will be set with the current time when the record is created. - If there is a
timestamp
column namedupdated_at
, and aUpdatedAt time.Time
attribute on thestruct
, it will be set with the current time when the record is updated. - Default database table names are lowercase, plural, and underscored versions of the
struct
name. Examples: User{} is "users", FooBar{} is "foo_bars", etc...
- PostgreSQL (>= 9.3)
- MySQL (>= 5.7)
- SQLite (>= 3.x)
- CockroachDB (>= 1.1.1)
Pop is easily configured using a YAML file. The configuration file should be stored in config/database.yml
or database.yml
.
development:
dialect: "postgres"
database: "your_db_development"
host: "localhost"
port: "5432"
user: "postgres"
password: "postgres"
test:
dialect: "mysql"
database: "your_db_test"
host: "localhost"
port: "3306"
user: "root"
password: "root"
staging:
dialect: "sqlite3"
database: "./staging.sqlite"
production:
dialect: "postgres"
url: {{ env "DATABASE_URL" }}
Note that the database.yml
file is also a Go template, so you can use Go template syntax. There are two special functions that are included, env
and envOr
.
env
- This function will look for the named environment variable and insert it into your file. This is useful for configuring production databases without having to store secret information in your repository.{{ env "DATABASE_URL" }}
envOr
- This function will look for the named environment variable and use it. If the variable can not be found a default value will be used.{{ envOr "MYSQL_HOST" "localhost" }}
You can generate a default configuration file using the init
command:
$ soda g config
The default will generate a database.yml
file in the current directory for a PostgreSQL database. You can override the type of database using the -t
flag and passing in any of the supported database types: postgres
, cockroach
, mysql
, or sqlite3
.
CockroachDB currently works best if you DO NOT use a URL and instead define each key item. Because CockroachDB more or less uses the same driver as postgres you have the same configuration options for both. In production you will also want to make sure you are using a secure cluster and have set all the needed connection parameters for said secure connection. If you do not set the sslmode or set it to disable
this will put dump and load commands into --insecure
mode.
Once you have a configuration file defined you can easily connect to one of these connections in your application.
db, err := pop.Connect("development")
if err != nil {
log.Panic(err)
}
Now that you have your connection to the database you can start executing queries against it.
Pop features CLI support via the soda
command for the following operations:
Available Commands:
create Creates databases for you
drop Drops databases for you
generate
help Help about any command
migrate Runs migrations against your database.
schema Tools for working with your database schema
Without sqlite 3 support:
$ go get github.com/gobuffalo/pop/...
$ go install github.com/gobuffalo/pop/soda
With sqlite 3 support:
$ go get -u -v -tags sqlite github.com/gobuffalo/pop/...
$ go install -tags sqlite github.com/gobuffalo/pop/soda
If you're not building your code with buffalo build
, you'll also have to pass -tags sqlite
to go build
when building your program.
Assuming you defined a configuration file like that described in the above section you can automatically create those databases using the soda
command:
$ soda create -a
$ soda create -e development
Assuming you defined a configuration file like that described in the above section you can automatically drop those databases using the soda
command:
$ soda drop -a
$ soda drop -e development
The soda
command supports the generation of models.
A full list of commands available for model generation can be found by asking for help:
$ soda generate help
The soda
command will generate Go models and, optionally, the associated migrations for you.
$ soda generate model user name:text email:text
Running this command will generate the following files:
models/user.go
models/user_test.go
migrations/20170115024143_create_users.up.fizz
migrations/20170115024143_create_users.down.fizz
The models/user.go
file contains a structure named User
with fields ID
, CreatedAt
, UpdatedAt
, Name
, and Email
. The first three correspond to the columns commonly found in ActiveRecord models as mentioned before, and the last two correspond to the additional fields specified on the command line. The known types are:
text
(string
in Go)blob
([]byte
in Go)time
ortimestamp
(time.Time
)nulls.Text
(nulls.String
) which corresponds to a nullifyable string, which can be distinguished from an empty stringuuid
(uuid.UUID
)- Other types are passed thru and are used as Fizz types.
The models/user_test.go
contains tests for the User model and they must be implemented by you.
The other two files correspond to the migrations as explained below. By default, it generates .fizz
files but you can also generate .sql
files by adding the flag --migration-type sql
to the command. Be aware, that you will need to specify the appropriate environment, because .sql
files are for specific databases.
$ soda generate model user name:text email:text --migration-type sql -e development
If development
is associated with a postgresql
configuration, running this command will generate the following files:
models/user.go
models/user_test.go
migrations/20170115024143_create_users.postgres.up.sql
migrations/20170115024143_create_users.postgres.down.sql
The soda
command supports the creation and running of migrations.
A full list of commands available for migration can be found by asking for help:
$ soda migrate --help
The soda
command will generate SQL migrations (both the up and down) files for you.
$ soda generate fizz name_of_migration
Running this command will generate the following files:
./migrations/20160815134952_name_of_migration.up.fizz
./migrations/20160815134952_name_of_migration.down.fizz
The generated files are fizz
files. Fizz lets you use a common DSL for generating migrations. This means the same .fizz
file can be run against any of the supported dialects of Pop! Find out more about Fizz
If you want to generate old fashion .sql
files you can use the -t
flag for that:
$ soda generate sql name_of_migration
Running this command will generate the following files:
./migrations/20160815134952_name_of_migration.up.sql
./migrations/20160815134952_name_of_migration.down.sql
The soda migrate
command supports both .fizz
and .sql
files, so you can mix and match them to suit your needs.
The soda
command will run the migrations using the following command:
$ soda migrate up
Migrations will be run in sequential order.
Migrations can also be run in reverse to rollback the schema.
$ soda migrate down
The previously run migrations will be kept track of in a table named schema_migration
in the database. The table name can be configured by setting migration_table_name
of the configuration options. The example below will use migrations
as the table name.
development:
dialect: "postgres"
url: "your_db_development"
options:
migration_table_name: migrations
user := User{}
err := db.Find(&user, id)
users := []User{}
err := db.All(&users)
err = db.Where("id in (?)", 1, 2, 3).All(&users)
// Last() orders by created_at
user := models.User{}
err := tx.Last(&user)
users := []models.User{}
query := db.Where("id = 1").Where("name = 'Mark'")
err := query.All(&users)
err = tx.Where("id in (?)", 1, 2, 3).All(&users)
err = db.Where("id in (?)", 1, 2, 3).All(&users)
err = db.Where("id in (?)", 1, 2, 3).Where("foo = ?", "bar").All(&users)
Unfortunately, for a variety of reasons you can't use an and
query in the same Where
call as an in
query.
// does not work:
err = db.Where("id in (?) and foo = ?", 1, 2, 3, "bar").All(&users)
// works:
err = db.Where("id in (?)", 1, 2, 3).Where("foo = ?", "bar").All(&users)
Select
allows you to load specific columns from a table. Useful when you don't want all columns from a table to be loaded in a query.
err = db.Select("name").All(&users)
// SELECT name FROM users
err = db.Select("max(age)").All(&users)
// SELECT max(age) FROM users
err = db.Select("age", "name").All(&users)
// SELECT age, name FROM users
// page: page number
// perpage: limit
roles := []models.UserRole{}
query := models.DB.LeftJoin("roles", "roles.id=user_roles.role_id").
LeftJoin("users u", "u.id=user_roles.user_id").
Where(`roles.name like ?`, name).Paginate(page, perpage)
count, _ := query.Count(models.UserRole{})
count, _ := query.CountByField(models.UserRole{}, "*")
sql, args := query.ToSQL(&pop.Model{Value: models.UserRole{}}, "user_roles.*",
"roles.name as role_name", "u.first_name", "u.last_name")
err := models.DB.RawQuery(sql, args...).All(&roles)
// Create one record.
user := models.User{}
user.Name = "Mark"
err := tx.Create(&user)
// Create many records.
users := models.Users{
{Name:"Mark"},
{Name: "Larry"},
}
err := tx.Create(&users)
The Save
method will attempt to create the record if the ID
is empty. If there is an ID
set it will attempt to update the record with that ID in the database.
// Save one record.
user := models.User{}
user.Name = "Mark"
err := tx.Save(&user)
// Save many records.
users := models.Users{
{Name:"Mark"},
{Name: "Larry"},
}
err := tx.Save(&users)
// Update one record.
user := models.User{}
user.Name = "Mark"
err := tx.Create(&user)
user.Name = "Mark Bates"
err = tx.Update(&user)
// Update many records.
users := models.Users{
{Name:"Mark"},
{Name: "Larry"},
}
err := tx.Create(&users)
users[0].Name = "Mark Bates"
users[1].Name = "Larry Morales"
err := tx.Update(&users)
// Destroy one record.
user := models.User{}
user.Name = "Mark"
err := tx.Create(&user)
err = tx.Destroy(&user)
// Destroy many records.
users := models.Users{
{Name:"Mark"},
{Name: "Larry"},
}
err := tx.Create(&users)
err = tx.Destroy(&users)
Pop allows you to perform an eager loading for associations defined in a model. By using pop.Connection.Eager()
function plus some fields tags predefined in your model you can extract associated data from a model.
type User struct {
ID uuid.UUID
Email string
Password string
Books Books `has_many:"books" order_by:"title asc"`
FavoriteSong Song `has_one:"song" fk_id:"u_id"`
Houses Addresses `many_to_many:"users_addresses"`
}
type Book struct {
ID uuid.UUID
Title string
Isbn string
User User `belongs_to:"user"`
UserID uuid.UUID
Writers Writers `has_many:"writers"`
}
type Writer struct {
ID uuid.UUID `db:"id"`
Name string `db:"name"`
BookID uuid.UUID `db:"book_id"`
Book Book `belongs_to:"book"`
}
type Song struct {
ID uuid.UUID
Title string
UserID uuid.UUID `db:"u_id"`
}
type Address struct {
ID uuid.UUID
Street string
HouseNumber int
}
type Addresses []Address
-
has_many: will load all records from the
books
table that have a column nameduser_id
or the column specified with fk_id that matches theUser.ID
value. -
belongs_to: will load a record from
users
table that have a column namedid
that matches withBook.UserID
value. -
has_one: will load a record from the
songs
table that have a column nameduser_id
or the column specified with fk_id that matches theUser.ID
value. -
many_to_many: will load all records from the
addresses
table through the tableusers_addresses
. Tableusers_addresses
MUST defineaddress_id
anduser_id
columns to matchUser.ID
andAddress.ID
values. You can also define a fk_id tag that will be used in the target association i.eaddresses
table. -
fk_id: defines the column name in the target association that matches model
ID
. In the example aboveSong
has a column namedu_id
that representsid
ofusers
table. When loadingFavoriteSong
,u_id
will be used instead ofuser_id
. -
order_by: used in
has_many
andmany_to_many
to indicate the order for the association when loading. The format to use isorder_by:"<column_name> <asc | desc>"
u := Users{}
// preload all associations for user with name 'Mark', i.e Books, Houses and FavoriteSong
err := tx.Eager().Where("name = 'Mark'").All(&u)
// preload only Books association for user with name 'Mark'.
err = tx.Eager("Books").Where("name = 'Mark'").All(&u)
Pop allows you to eager loading nested associations by using .
character to concatenate them. Take a look at the example bellow.
// will load all Books for u and for every Book will load the user which will be the same as u.
tx.Eager("Books.User").First(&u)
// will load all Books for u and for every Book will load all Writers.
tx.Eager("Books.Writers").First(&u)
// will load all Books for u and for every Book will load all Writers and for every writer will load the Book association.
tx.Eager("Books.Writers.Book").First(&u)
// will load all Books for u and for every Book will load all Writers. And Also it will load the favorite song for user.
tx.Eager("Books.Writers").Eager("FavoriteSong").First(&u)
Pop allows you to create models and their associations in one step. You no longer need to create every association separately anymore. Pop will even create join table records for many_to_many
associations.
Assuming the following pieces of pseudo-code:
user := User{
Name: "Mark Bates",
Books: Books{{Title: "Pop Book", Description: "Pop Book", Isbn: "PB1"}},
FavoriteSong: Song{Title: "Don't know the title"},
Houses: Addresses{
Address{HouseNumber: 1, Street: "Golang"},
},
}
err := tx.Eager().Create(&user)
-
It will notice
Books
is ahas_many
association and it will realize that to actually store every book it will need to get theUser ID
first. So, it proceeds to store firstUser
data so it can retrieve an ID and then use that ID to fillUserID
field in everyBook
inBooks
. Later it stores all books in database. -
FavoriteSong
is ahas_one
association and it uses same logic described inhas_many
association. SinceUser
data was previously saved before creating all books, it already knows thatUser
got anID
so it fills itsUserID
field with that value andFavoriteSong
is then stored in database. -
Houses
in this example is amany_to_many
relationship and it will have to deal with two tables in this case:users
andaddresses
. It will need to store all addresses first inaddresses
table before save them in the many to many table. BecauseUser
was already stored, it already have anID
. * This is a special case to deal with, since this behavior is different to all other associations, it is solved by implementing theAssociationCreatableStatement
interface, all other associations implement by defaultAssociationCreatable
interface.
For a belongs_to
association like shown in the example below, it will need first to create User
to retrieve ID value and then fill its UserID
field before be saved in database.
book := Book{
Title: "Pop Book",
Description: "Pop Book",
Isbn: "PB1",
User: User{
Name: nulls.NewString("Larry"),
},
}
tx.Eager().Create(&book)
All these cases are assuming that none of models and associations has previously been saved in database.
Pop provides a means to execute code before and after database operations. This is done by defining specific methods on your models. For example, to hash a user password you may want to define the following method:
type User struct {
ID uuid.UUID
Email string
Password string
}
func (u *User) BeforeSave(tx *pop.Connection) error {
hash, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
if err != nil {
return errors.WithStack(err)
}
u.Password = string(hash)
return nil
}
- BeforeSave
- BeforeCreate
- BeforeUpdate
- BeforeDestroy
- AfterSave
- AfterCreate
- AfterUpdate
- AfterDestroy
- AfterFind
The Unofficial pop Book: a gentle introduction to new users.