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

Kết quả trên database:

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.

Kết quả bảng database:

Kết quả database:

Kết quả database

3.2 Chạy các API GET

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
            }
        ]
    }
]
[
    {
        "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
        }
    }
]
[
    {
        "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.