Hiểu về go-pg trong golang qua ví dụ mẫu về blog - P1
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.
All rights reserved