Skip to content

Commit ee2a07a

Browse files
En/migration (#272)
1 parent b88c729 commit ee2a07a

File tree

15 files changed

+632
-1
lines changed

15 files changed

+632
-1
lines changed

Diff for: docs/advanced-guide/handling-data-migrations/page.md

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# Handling Data Migrations
2+
3+
Gofr supports data migrations for MySQL and Redis which allows to alter the state of a database, be it adding a new column to existing table or modifying the data type of an existing column or adding constraints to an existing table, setting and removing keys etc.
4+
5+
**How Migrations help?**
6+
7+
Suppose you manually edit fragments of your database, and now it's your responsibility to inform other developers to execute them. Additionally, you need to keep track of which changes should be applied to production machines in the next deployment.
8+
9+
GoFr maintains the table called **gofr_migration** which helps in such case. This table tracks which migrations have already been executed and ensures that only migrations that have never been run are executed. This way, you only need to ensure that your migrations are properly in place. ([Learn more](https://cloud.google.com/architecture/database-migration-concepts-principles-part-1))
10+
11+
## Usage
12+
13+
We will create an employee table using migrations.
14+
15+
### Creating Migration Files
16+
17+
It is recommended to maintain a migrations directory in your project root to enhance readability and maintainability.
18+
19+
**Migration file names**
20+
21+
It is recommended that each migration file should be numbered using the unix timestamp when the migration was created, This helps prevent numbering conflicts when working in a team environment.
22+
23+
Create the following file in migrations directory.
24+
25+
**Filename : 1708322067_create_employee_table.go**
26+
```go
27+
package migrations
28+
29+
import "gofr.dev/pkg/gofr/migration"
30+
31+
32+
const createTable = `CREATE TABLE IF NOT EXISTS employee
33+
(
34+
id int not null
35+
primary key,
36+
name varchar(50) not null,
37+
gender varchar(6) not null,
38+
contact_number varchar(10) not null
39+
);`
40+
41+
func createTableEmployee() migration.Migrate {
42+
return migration.Migrate{
43+
UP: func(d migration.Datasource) error {
44+
_, err := d.DB.Exec(createTable)
45+
if err != nil {
46+
return err
47+
}
48+
return nil
49+
},
50+
}
51+
}
52+
```
53+
54+
`migration.Datasource` have the datasources whose migrations are supported which are Redis and MySQL.
55+
All the migrations run in transactions by default.
56+
57+
For MySQL it is highly recommended to use `IF EXISTS` and `IF NOT EXIST` in DDL commands as MySQL implicitly commits these Commands.
58+
59+
**Create a function which returns all the migrations in a map**
60+
61+
**Filename : all.go**
62+
```go
63+
package migrations
64+
65+
import "gofr.dev/pkg/gofr/migration"
66+
67+
func All() map[int64]migration.Migrate {
68+
return map[int64]migration.Migrate{
69+
1708322067: createTableEmployee(),
70+
}
71+
}
72+
```
73+
74+
Migrations will run in ascending order of keys in this map.
75+
76+
### Initialisation from main.go
77+
```go
78+
package main
79+
80+
import (
81+
"errors"
82+
"fmt"
83+
84+
"gofr.dev/examples/using-migrations/migrations"
85+
"gofr.dev/pkg/gofr"
86+
)
87+
88+
func main() {
89+
// Create a new application
90+
a := gofr.New()
91+
92+
// Add migrations to run
93+
a.Migrate(migrations.All())
94+
95+
// Run the application
96+
a.Run()
97+
}
98+
99+
```
100+
101+
When we run the app we will see the following logs for migrations which ran successfully.
102+
103+
```bash
104+
INFO [16:55:46] Migration 1708322067 ran successfully
105+
```
106+
107+
108+
109+

Diff for: docs/navigation.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const navigation = [
1515
{ title: 'Inter-service HTTP Calls', href: '/docs/advanced-guide/interservice-http-call' },
1616
{ title: 'Publishing Custom Metrics', href: '/docs/advanced-guide/publishing-custom-metrics' },
1717
{ title: 'Remote Log Level Change', href: '/docs/advanced-guide/remote-log-level-change' },
18-
// { title: 'Handling Data Migrations', href: '/docs/advanced-guide/data-migrations' },
18+
{ title: 'Handling Data Migrations', href: '/docs/advanced-guide/handling-data-migrations' },
1919
// { title: 'Dealing with Remote Files', href: '/docs/advanced-guide/remote-files' },
2020
// { title: 'Dynamic Configurations', href: '/docs/advanced-guide/dynamic-configs' },
2121
// { title: 'Supporting OAuth', href: '/docs/advanced-guide/oauth' },

Diff for: examples/using-migrations/configs/.env

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
APP_NAME=using-migrations
2+
HTTP_PORT=9000
3+
4+
REDIS_HOST=localhost
5+
REDIS_PORT=2002
6+
7+
DB_HOST=localhost
8+
DB_USER=root
9+
DB_PASSWORD=password
10+
DB_NAME=gofr
11+
DB_PORT=2001
12+
13+
TRACER_HOST=localhost
14+
TRACER_PORT=2005

Diff for: examples/using-migrations/main.go

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"gofr.dev/examples/using-migrations/migrations"
8+
"gofr.dev/pkg/gofr"
9+
)
10+
11+
const (
12+
queryGetEmployee = "SELECT id,name,gender,contact_number,dob from employee where name = ?"
13+
queryInsertEmployee = "INSERT INTO employee (id, name, gender, contact_number,dob) values (?, ?, ?, ?, ?)"
14+
)
15+
16+
func main() {
17+
// Create a new application
18+
a := gofr.New()
19+
20+
// Add migrations to run
21+
a.Migrate(migrations.All())
22+
23+
// Add all the routes
24+
a.GET("/employee", GetHandler)
25+
a.POST("/employee", PostHandler)
26+
27+
// Run the application
28+
a.Run()
29+
}
30+
31+
type Employee struct {
32+
ID int `json:"id"`
33+
Name string `json:"name"`
34+
Gender string `json:"gender"`
35+
Phone int `json:"contact_number"`
36+
DOB string `json:"dob"`
37+
}
38+
39+
// GetHandler handles GET requests for retrieving employee information
40+
func GetHandler(c *gofr.Context) (interface{}, error) {
41+
name := c.Param("name")
42+
if name == "" {
43+
return nil, errors.New("name can't be empty")
44+
}
45+
46+
row := c.DB.QueryRowContext(c, queryGetEmployee, name)
47+
if row.Err() != nil {
48+
return nil, errors.New(fmt.Sprintf("DB Error : %v", row.Err()))
49+
}
50+
51+
var emp Employee
52+
53+
err := row.Scan(&emp.ID, &emp.Name, &emp.Gender, &emp.Phone, &emp.DOB)
54+
if err != nil {
55+
return nil, errors.New(fmt.Sprintf("DB Error : %v", err))
56+
}
57+
58+
return emp, nil
59+
}
60+
61+
// PostHandler handles POST requests for creating new employees
62+
func PostHandler(c *gofr.Context) (interface{}, error) {
63+
var emp Employee
64+
if err := c.Bind(&emp); err != nil {
65+
c.Logger.Errorf("error in binding: %v", err)
66+
return nil, errors.New("invalid body")
67+
}
68+
69+
// Execute the INSERT query
70+
_, err := c.DB.ExecContext(c, queryInsertEmployee, emp.ID, emp.Name, emp.Gender, emp.Phone, emp.DOB)
71+
72+
if err != nil {
73+
return Employee{}, errors.New(fmt.Sprintf("DB Error : %v", err))
74+
}
75+
76+
return fmt.Sprintf("succesfully posted entity : %v", emp.Name), nil
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package migrations
2+
3+
import (
4+
"gofr.dev/pkg/gofr/migration"
5+
)
6+
7+
const createTable = `CREATE TABLE IF NOT EXISTS employee
8+
(
9+
id int not null
10+
primary key,
11+
name varchar(50) not null,
12+
gender varchar(6) not null,
13+
contact_number varchar(10) not null
14+
);`
15+
16+
const employee_date = `INSERT INTO employee (id, name, gender, contact_number) VALUES (1, 'Umang', "M", "0987654321");`
17+
18+
func createTableEmployee() migration.Migrate {
19+
return migration.Migrate{
20+
UP: func(d migration.Datasource) error {
21+
_, err := d.DB.Exec(createTable)
22+
if err != nil {
23+
return err
24+
}
25+
26+
_, err = d.DB.Exec(employee_date)
27+
if err != nil {
28+
return err
29+
}
30+
31+
_, err = d.DB.Exec("alter table employee add dob varchar(11) null;")
32+
if err != nil {
33+
return err
34+
}
35+
36+
return nil
37+
},
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package migrations
2+
3+
import (
4+
"context"
5+
"gofr.dev/pkg/gofr/migration"
6+
)
7+
8+
func addEmployeeInRedis() migration.Migrate {
9+
return migration.Migrate{
10+
UP: func(d migration.Datasource) error {
11+
err := d.Redis.Set(context.Background(), "Umang", "0987654321", 0).Err()
12+
if err != nil {
13+
return err
14+
}
15+
16+
return nil
17+
18+
},
19+
}
20+
}

Diff for: examples/using-migrations/migrations/all.go

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package migrations
2+
3+
import (
4+
"gofr.dev/pkg/gofr/migration"
5+
)
6+
7+
func All() map[int64]migration.Migrate {
8+
return map[int64]migration.Migrate{
9+
1708322067: createTableEmployee(),
10+
1708322089: addEmployeeInRedis(),
11+
}
12+
}

Diff for: go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/DATA-DOG/go-sqlmock v1.5.2
77
github.com/alicebob/miniredis/v2 v2.31.1
88
github.com/go-sql-driver/mysql v1.7.1
9+
github.com/gogo/protobuf v1.3.2
910
github.com/gorilla/mux v1.8.1
1011
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
1112
github.com/joho/godotenv v1.5.1

Diff for: go.sum

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
4545
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
4646
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
4747
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
48+
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
4849
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
4950
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
5051
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=

Diff for: pkg/gofr/gofr.go

+5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"gofr.dev/pkg/gofr/container"
2222
"gofr.dev/pkg/gofr/logging"
2323
"gofr.dev/pkg/gofr/metrics"
24+
"gofr.dev/pkg/gofr/migration"
2425
"gofr.dev/pkg/gofr/service"
2526
)
2627

@@ -208,6 +209,10 @@ func (a *App) SubCommand(pattern string, handler Handler) {
208209
a.cmd.addRoute(pattern, handler)
209210
}
210211

212+
func (a *App) Migrate(migrationsMap map[int64]migration.Migrate) {
213+
migration.Run(migrationsMap, a.container)
214+
}
215+
211216
func (a *App) initTracer() {
212217
tracerHost := a.Config.Get("TRACER_HOST")
213218
tracerPort := a.Config.GetOrDefault("TRACER_PORT", "9411")

Diff for: pkg/gofr/migration/datasource.go

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package migration
2+
3+
type Datasource struct {
4+
Logger
5+
6+
DB sqlDB
7+
Redis redis
8+
}

Diff for: pkg/gofr/migration/logger.go

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package migration
2+
3+
type Logger interface {
4+
Info(args ...interface{})
5+
Infof(format string, args ...interface{})
6+
Error(args ...interface{})
7+
Errorf(format string, args ...interface{})
8+
}

0 commit comments

Comments
 (0)