Database là một nền tảng quan trọng của mọi ngôn ngữ lập trình. Hãy thử tưởng tượng một trang web code đầy đủ, giao diện đẹp, nhưng thiếu đi cơ sở dữ liệu để lưu trữ thông tin đăng ký, đăng nhập người dùng, các bài viết, trang web đó sẽ có mà như không. Hôm nay mình sẽ giới thiệu cách kết nối và sử dụng cơ sở dữ liệu postgresql trong ngôn ngữ golang bằng ví dụ mẫu về blog đơn giản.
1. Cài đặt
1.1 Cài đặt package
Đầu tiên luôn là thêm package, ta sẽ dùng go-pg version 10
go get github.com/go-pg/pg/v10
1.2. Cấu trúc thư mục
├──config/
| ├──database.go
├──controller/
| ├──base.go
| ├──user.go
| ├──blog.go
| ├──comment.go
├──model
| ├──user.go
| ├──blog.go
| ├──comment.go
├──router/
| ├──routes.go
├──main.go -- this is where the app starts
├──...
2. Bắt đầu
Trong ví dụ này mình sẽ dùng Postman để truyền data gọi API mà không có giao diện.
Cứ cho rằng bạn đã chuẩn bị sẵn pgadmin có sẵn database, giờ ta chỉ việc code kết nối và thao tác với database bằng package go-pg này.
2.1 Kết nối cơ sở dữ liệu, tạo model
config/database: tạo func kết nối cơ sở dữ liệu
package config import ( "golangpostgre/model" "github.com/go-pg/pg/v10/orm" "github.com/go-pg/pg/v10"
) func ConnectDatabase() (db *pg.DB){ db = pg.Connect(&pg.Options{ //Kết nối database gồm nhập addr, user, password và database Addr: ":5432", User: "postgres", Password: "123456", Database: "postgres", }) err := createSchema(db) //Nếu đã có đầy đủ bảng database có thể bỏ qua bước này if err != nil{ panic(err) } return db
} func createSchema(db *pg.DB) error { models := []interface{}{ //đây là những model sẽ được định nghĩa ra (*model.User)(nil), (*model.Post)(nil), (*model.Comment)(nil), } for _, model := range models { //Tạo từng bảng trong database err := db.Model(model).CreateTable(&orm.CreateTableOptions{ Temp: false, //Nếu đặt Temp: true sẽ biến thành in-memory database IfNotExists: true, //Nếu tồn tại bảng sẽ không tạo thêm }) if err != nil { return err } } return nil
}
Bạn có thể chọn định nghĩa model rồi chạy lệnh tạo bảng lên database (như trong ví dụ này sẽ làm để giúp bạn hiểu về go-pg), hoặc có thể tự tạo bảng định nghĩa các cột bên ngoài và bỏ qua bước createschema trên.
In-memory database có thể hiểu đơn giản ở đây là bảng được tạo ra và lưu trữ tạm thời khi chương trình đang chạy, khi bạn tắt chương trình đi toàn bộ bảng và bản ghi sẽ biến mất và bạn lại tạo lại từ đầu.
model/user.go: định nghĩa model user
package model type User struct{ tableName struct{} `pg:"auth.users"` //bảng users có schema auth Id int `pg:"type:serial,pk"` //trường id primary key, auto_increment. FirstName string //trường first_name text LastName string //trường last_name text Email string `pg:",unique"` //trường email không được trùng lặp Password string //trường password text Posts []Post `pg:"rel:has-many"` //quan hệ một nhiều với model Post
}
Lưu ý: khi một trường có tên là id được tạo ra sẽ là primary key
model/blog.go: định nghĩa model post
package model import "time" type Post struct{ tableName struct{} `pg:"blog.post"` Id int `pg:"type:serial" ` Content string `pg:",notnull"` //trường content text khác null Title string `pg:",notnull"` CreatedAt time.Time `pg:"type:timestamp without time zone,default:now()"` //trường created_at mặc định là thời điểm hiện tại UpdatedAt time.Time `pg:"type:timestamp without time zone"` UserId int `pg:"type:integer"` //nếu để mặc định sẽ là bigint, ta override thành integer User User `pg:"rel:has-one"` //Post quan hệ nhiều một với User
}
model/comment.go: định nghĩa model comment
package model import "time" type Comment struct{ tableName struct{} `pg:"blog.comment"` Id int `pg:"type:serial"` Content string `pg:",notnull"` CreatedAt time.Time `pg:"type:timestamp without time zone,default:now()"` UserId int `pg:"type:integer,notnull"` User *User `pg:"rel:has-one"` PostId int `pg:"type:integer,notnull"` Post *Post `pg:"rel:has-one"`
}
2.2 Tạo các route API
controller/base.go: khai báo DB tại controller
package controller import "github.com/go-pg/pg/v10" var DB *pg.DB
Biến DB trên sẽ sử dụng xuyên suốt ví dụ
router/routes.go: định nghĩa các route.
package router import ( "golangpostgre/controller" "github.com/kataras/iris/v12"
) func AllRoutes(app *iris.Application){ app.Get("/api/user",controller.GetUsers) //Lấy tất cả user app.Get("/api/user/{userId}",controller.GetUserById) //Lấy một user app.Post("/api/register",controller.Register) //Tạo user mới app.Put("/api/user/{id}",controller.UpdateUser) //Chỉnh sửa user app.Get("/api/posts",controller.GetPosts) //Lấy tất cả post app.Get("/api/posts/{postId}",controller.GetPostById) //Lấy một post app.Post("/api/post/create",controller.CreatePost) //tạo post mới app.Put("/api/post/{id}",controller.UpdatePost) //chỉnh sửa post app.Delete("/api/post/{id}",controller.DeletePost) //Xóa post app.Get("/api/comment",controller.GetComments) //Lấy tất cả comment app.Get("/api/comment/{commentId}",controller.GetCommentById) //Lấy một comment app.Post("/api/comment/create",controller.CreateComment) //Tạo comment app.Put("/api/comment/{id}",controller.UpdateComment) //Chỉnh sửa comment app.Delete("/api/comment/{id}",controller.DeleteComment) //Xóa comment
}
Các function trong package controller trên sẽ dùng để thực hiện các nghiệp vụ xử lý logic với bảng database.
2.3 Viết code để khởi chạy
main.go:
package main import ( "golangpostgre/config" "golangpostgre/controller" "golangpostgre/router" "github.com/kataras/iris/v12"
) func main() { db := config.ConnectDatabase() //Kết nối database defer db.Close() //Đóng database trước kết thúc chương trình app := iris.New() //Sử dụng framework iris controller.DB = db router.AllRoutes(app) //Đăng ký các route app.Run(iris.Addr(":8080")) //chạy ở cổng 8080 }
2.4 Thực hiện xử lý logic cho từng route.
controller/user.go: xử lý cho từng route liên quan user
package controller import ( "golangpostgre/model" "github.com/go-pg/pg/v10" "github.com/kataras/iris/v12" "golang.org/x/crypto/bcrypt"
) func GetUsers(ctx iris.Context){ // GET http://localhost:8080/api/user var user []model.User //Select toàn bộ user, .Relation có thể thêm vào để lấy các Post của user err := DB.Model(&user).Relation("Posts").Select() if err != nil{ ctx.StatusCode(iris.StatusInternalServerError) return } ctx.JSON(user)
} func GetUserById(ctx iris.Context){ // GET http://localhost:8080/api/user/{userId} id := ctx.Params().Get("userId") //Lấy giá trị userId truyền vào var user model.User //Select user với id err := DB.Model(&user).Relation("Posts").Where("id = ?",id).Select() if err != nil{ ctx.StatusCode(iris.StatusInternalServerError) return } ctx.JSON(user)
} func Register(ctx iris.Context){ //POST http://localhost:8080/api/register var data map[string]string ctx.ReadJSON(&data) if data["password"] != data["passwordconfirm"]{ //check password có khớp passwordconfirm không ctx.StatusCode(400) ctx.JSON(map[string]string{ "message":"password doesn't match", }) return } password, _ :=bcrypt.GenerateFromPassword([]byte(data["password"]),14) //mã hóa password user := model.User{ FirstName: data["first_name"], LastName: data["last_name"], Email:data["email"], Password:string(password), } _,err := DB.Model(&user).Insert() //Insert vào bảng users if err != nil{ panic(err) } ctx.JSON(user)
} func UpdateUser(ctx iris.Context){ //PUT http://localhost:8080/api/user/{id} id := ctx.Params().Get("id") var data map[string]interface{} ctx.ReadJSON(&data) //Đọc dữ liệu truyền vào qua map string interface, các trường trong map dùng để update bảng user //Update bảng user _,err := DB.Model(&data).TableExpr("auth.users").Where("id = ?",id).Update() if err != nil{ ctx.StatusCode(iris.StatusInternalServerError) return } ctx.StatusCode(iris.StatusOK) ctx.JSON("Cập nhật thành công")
}
Lưu ý: Update, Select, Insert truyền *map[string]interface{}
như bên trên chỉ áp dụng cho go-pg v10 trở lên.
controller/blog.go: xử lý cho từng route liên quan post
package controller import ( "golangpostgre/model" "log" "github.com/kataras/iris/v12"
) func GetPosts(ctx iris.Context){ // GET http://localhost:8080/api/posts var posts []model.Post err := DB.Model(&posts).Relation("User").Select() if err != nil{ panic(err) } ctx.JSON(posts)
} func GetPostById(ctx iris.Context){ // GET http://localhost:8080/api/posts/{postId} id := ctx.Params().Get("userId") var post model.Post err := DB.Model(&post).Relation("User").Where("id = ?",id).Select() if err != nil{ ctx.StatusCode(iris.StatusInternalServerError) return } ctx.JSON(post)
} func CreatePost(ctx iris.Context){ // POST http://localhost:8080/api/post/create var data map[string]interface{} ctx.ReadJSON(&data) data["user_id"]=1 //Truyền thêm tham số chưa có khi đọc dữ liệu _,err := DB.Model(&data).TableExpr("blog.post").Insert() if err != nil{ log.Println(err) ctx.StatusCode(500) return } ctx.JSON("Tạo bài viết thành công")
} func UpdatePost(ctx iris.Context){ // PUT http://localhost:8080/api/post/{id} var data map[string]interface{} ctx.ReadJSON(&data) id := ctx.Params().Get("id") data["updated_at"]=time.Now() _,err := DB.Model(&data).TableExpr("blog.post").Where("id = ?",id).Update() if err != nil{ log.Println(err) ctx.StatusCode(iris.StatusInternalServerError) return } ctx.JSON("Cập nhật thành công")
} func DeletePost(ctx iris.Context){ // DELETE http://localhost:8080/api/post/{id} id:= ctx.Params().Get("id") post := new(model.Post) //tương ứng với = (*model.Post)(nil) //Xóa post với id _,err := DB.Model(post).Where("id = ?", id).Delete() if err != nil{ log.Println(err) ctx.StatusCode(iris.StatusInternalServerError) return } ctx.JSON("Xóa thành công")
}
Lưu ý: Đây chỉ là ví dụ mẫu giúp hiểu bản chất của go-pg, nên ở trên user_id được gán cho một giá trị cố định = 1 qua việc xem thông tin bảng database. Nhiều chỗ cũng tương tự ở bên dưới. Nên thay đổi giá trị phù hợp với bảng database.
controller/comment.go: xử lý cho từng route liên quan comment
package controller import ( "golangpostgre/model" "log" "time" "github.com/kataras/iris/v12"
) func GetComments(ctx iris.Context){ // GET http://localhost:8080/api/comment var comments []model.Comment err := DB.Model(&comments).Relation("User").Relation("Post").Select() if err != nil{ panic(err) } ctx.JSON(comments)
} func GetCommentById(ctx iris.Context){ // GET http://localhost:8080/api/comment/{commentId} id := ctx.Params().Get("userId") var comment model.Comment err := DB.Model(&comment).Relation("Post").Relation("User").Where("id = ?",id).Select() if err != nil{ ctx.StatusCode(iris.StatusInternalServerError) return } ctx.JSON(comment)
} func UpdateComment(ctx iris.Context){ // PUT http://localhost:8080/api/comment/{id} var data map[string]interface{} ctx.ReadJSON(&data) id:= ctx.Params().Get("id") _,err := DB.Model(&data).TableExpr("blog.comment").Where("id = ?",id).Update() if err != nil{ log.Println(err) ctx.StatusCode(iris.StatusInternalServerError) return } ctx.JSON("Update thành công")
} func DeleteComment(ctx iris.Context){ // DELETE http://localhost:8080/api/comment/{id} id:= ctx.Params().Get("id") _,err := DB.Model((*model.Comment)(nil)).Where("id = ?",id).Delete() if err != nil{ log.Println(err) ctx.StatusCode(iris.StatusInternalServerError) return } ctx.JSON("Xóa thành công")
} func CreateComment(ctx iris.Context){ // POST http://localhost:8080/api/comment/create var data map[string]interface{} ctx.ReadJSON(&data) data["user_id"]=1 data["post_id"]=1 _,err := DB.Model(&data).TableExpr("blog.comment").Insert() if err != nil{ log.Println(err) ctx.StatusCode(iris.StatusInternalServerError) return } ctx.JSON("Comment thành công")
}
3. Chạy code
Khi bắt đầu chạy, chương trình sẽ tạo ra các bảng database (nếu chưa có) với các trường, constraint, quan hệ ứng với các model được định nghĩa. Tuy nhiên các bảng được định nghĩa trong model có tên sau: auth.users
, blog.post
, blog.comment
auth, blog chính là các schema, nên nếu trong database bạn chưa có hay thêm các schema này như sau: vào Servers → PostgreSQL 13 →Databases → postgres → click chuột phải schemas → Create → Schemas → Nhập tên schema rồi bấm lưu vào hộp thoại như bên dưới đây
Giờ chạy chương trình và ta đã có bảng với các trường tương ứng. Ví dụ auth.users:
3.1 Chạy API POST, PUT, DELETE
- Tạo user mới: POST http://localhost:8080/api/register
Kết quả trên database:
- Cập nhật user: PUT http://localhost:8080/api/user/{id}
-
Tạo post mới: POST http://localhost:8080/api/post/create
Trường hợp không truyền content
Lỗi như sau:
Do chúng ta đã buộc title, content khác null, nên ta phải bổ sung thêm content.
Kết quả bảng database:
Bạn có thể tự tạo thêm bản ghi bằng việc gọi lại API trên truyền dữ liệu title, content khác.
- Cập nhật post: PUT http://localhost:8080/api/post/{id}
Kết quả bảng database:
-
Xóa post: DELETE http://localhost:8080/api/post/{id}
Chỉ cần nhập id post cần xóa là bạn sẽ xóa thành công.
-
Tạo comment mới: POST http://localhost:8080/api/comment/create
Kết quả database:
- Cập nhật comment: PUT http://localhost:8080/api/comment/{id}
Kết quả database
-
Xóa comment: DELETE http://localhost:8080/api/post/{id}
Tương tự như xóa post.
3.2 Chạy các API GET
-
Lấy danh sách user: GET http://localhost:8080/api/user
Trường hợp không có relation:
err := DB.Model(&user).Select()
Kết quả trả ra:
[ { "Id": 1, "FirstName": "Nguyễn Trần", "LastName": "Nhật Đức", "Email": "nhatduc@techmaster.vn", "Password": "$2a$14$HpP97XwOOVobmYsH7w6Ak.IEz5p0lAwanC/THXI.WFZy2exi2zA8K", "Posts": null }
]
Trường hợp có relation: err := DB.Model(&user).Relation("Posts").Select()
Kết quả trả ra:
[ { "Id": 1, "FirstName": "Nguyễn Trần", "LastName": "Nhật Đức", "Email": "nhatduc@techmaster.vn", "Password": "$2a$14$HpP97XwOOVobmYsH7w6Ak.IEz5p0lAwanC/THXI.WFZy2exi2zA8K", "Posts": [ { "Id": 2, "Content": "Content được chỉnh sửa .....", "Title": "Sử dụng logrus thay thế logging mặc định golang", "CreatedAt": "2021-07-20T17:03:14.513294Z", "UpdatedAt": "2021-07-20T10:17:13.82935Z", "UserId": 1, "User": null } ] }
]
-
Lấy danh sách post: GET http://localhost:8080/api/posts
Kết quả trả ra:
[ { "Id": 2, "Content": "Content được chỉnh sửa .....", "Title": "Sử dụng logrus thay thế logging mặc định golang", "CreatedAt": "2021-07-20T17:03:14.513294Z", "UpdatedAt": "2021-07-20T10:17:13.82935Z", "UserId": 1, "User": { "Id": 1, "FirstName": "Nguyễn Trần", "LastName": "Nhật Đức", "Email": "nhatduc@techmaster.vn", "Password": "$2a$14$HpP97XwOOVobmYsH7w6Ak.IEz5p0lAwanC/THXI.WFZy2exi2zA8K", "Posts": null } }
]
-
Lấy danh sách comment: GET http://localhost:8080/api/comment
Kết quả trả ra:
[ { "Id": 2, "Content": "thôi chả thấy hay nữa", "CreatedAt": "2021-07-20T17:33:23.811122Z", "UserId": 1, "User": { "Id": 1, "FirstName": "Nguyễn Trần", "LastName": "Nhật Đức", "Email": "nhatduc@techmaster.vn", "Password": "$2a$14$HpP97XwOOVobmYsH7w6Ak.IEz5p0lAwanC/THXI.WFZy2exi2zA8K", "Posts": null }, "PostId": 2, "Post": { "Id": 2, "Content": "Content được chỉnh sửa .....", "Title": "Sử dụng logrus thay thế logging mặc định golang", "CreatedAt": "2021-07-20T17:03:14.513294Z", "UpdatedAt": "2021-07-20T10:17:13.82935Z", "UserId": 1, "User": null } }
]
4. Kết
Phần này chủ yếu tập trung vào SELECT, UPDATE, DELETE và quan hệ một - nhiều, định nghĩa model. Các bạn hãy tự gõ lại code và chạy thử để nắm rõ hơn. Mong rằng với ví dụ trên sẽ giúp các bạn hiểu được căn bản về golang kết nối postgresql.
Phần sau ta sẽ ví dụ tiếp về quan hệ nhiều nhiều, bố sung thêm định nghĩa model và join các bảng với nhau.