Day18 Gin with GORM

What is ORM

ORM全名为Object-Relational Mapping 物件关系对应,也就是透过物件导向的方式将关系资料库的资料映射至物件的技术。

那ORM有三大核心准则分别是

  • 简单性
  • 传达性
  • 精确性

ORM的使用也带来了以下优点:

  • 隐藏了数据访问的细节,封闭通用数据库的交互访问,并将其封装成物件,使我们使用时完全不用写SQL Command。
  • ORM使我们构造固化数据结构变得简单易行。
  • ORM可以防止SQL-Injection
  • ORM使面对对象不需要编码,能够像使用物件一样简单的操作资料库。
  • ORM可以自动对实体对象与资料库中表进行字段与属性的映射,不需要单独的数据访问层就可以数据进行增删改查,并提高开发效率。
  • 方便data migration,当资料库发生改变时,不需要对模组进行修改,只需要修改映射关系即可。

同时ORM也隐含着以下缺点:

  • 自动化进行映射与关联管理,必然的会牺牲效能。然而现今大部分的ORM框架都已经用各式各样方法尽量地减少效能的牺牲了。
  • 操作ORM会隐藏了数据层面的业务逻辑,导致使用者对资料库设计方面掌握度较低。
  • 更为复杂的SQL Command,ORM并无法执行,到头来还是需要写Pure SQL。

GORM

GORM是专门为Gin这个Framework所设计的ORM Library,主要用於操作资料库。这边我们会基於过往的MVC架构将其依不同功能分成dao、model以及service三层。

Connect with Database

这边我们专门放连结database所用到的function与变数等。

dao/postgresql.go

package dao

import (
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

var (
	SqlSession *gorm.DB
)
func Initialize(dbConfig string) (*gorm.DB, error) {
	var err error
	SqlSession, err = gorm.Open(postgres.Open(dbConfig), &gorm.Config{})
	return SqlSession, err
}
  1. 首先我们创造一个全域变数叫做SqlSession,有点致敬db session的概念。
  2. 之後再写个function用来连接database

main.go

package main

import (
	"github.com/gin-gonic/gin"
	"github.com/joho/godotenv"
	"ironman-2021/app/config"
	"ironman-2021/app/dao"
	"ironman-2021/app/model"
	"os"
)

func main() {
	envErr := godotenv.Load()
	if envErr != nil {
		panic(envErr)
	}

	port := os.Getenv("PORT")
	dbConfig := os.Getenv("DB_CONFIG")
	db, ormErr := dao.Initialize(dbConfig)
	if ormErr != nil {
		panic(ormErr)
	}
	migrateErr := db.AutoMigrate(&model.User{})
	if migrateErr != nil {
		return 
	}
	
	server := gin.Default()
	server.GET("/hc", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "health check",
		})
	})
	config.RouteUsers(server)
	err := server.Run(":" + port)
	if err != nil {
		panic(err)
	}
}
  1. 首先,我们透过godotenv得到环境变数DB_CONFIG
  2. 之後则透过呼叫Initialize()来连接上资料库
  3. 最後则进行migration,将User这张table给create出来

model

这边则负责堆放需要在database所创建的table资料

model/user.go

package model

import (
	"gorm.io/gorm"
	"time"
)

type User struct {
	gorm.Model
	ID        int64    `gorm:"primary_key;auto_increment" json:"id"`
	Account string `gorm:"size:100;not null;unique" json:"account"`
	Password  string    `gorm:"size:100;not null;" json:"password"`
	Email     string    `gorm:"size:100;not null;unique" json:"email"`
	CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"`
	UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"`
}

我们create了一个名为User的gorm.Model,然後上面的primary key、field如程序码所述,这边就不在多做赘述。

service

这里则是关於orm相关的变数以及function,这边也是主要对资料库进行资料CRUD的地方。

service/user.go

package service

import (
	"fmt"
	"ironman-2021/app/dao"
	"ironman-2021/app/model"
)

var UserFields = []string{"id", "account", "email"}

func SelectOneUsers(id int64) (*model.User, error) {
	userOne:=&model.User{}
	err := dao.SqlSession.Select(UserFields).Where("id=?", id).First(&userOne).Error
	if err != nil {
		return nil, err
	} else {
		return userOne, nil
	}
}

func RegisterOneUser(account string, password string, email string) error {
	if !CheckOneUser(account) {
		return fmt.Errorf("User exists.")
	}
	user := model.User{
		Account: account,
		Password: password,
		Email: email,
	}
	insertErr := dao.SqlSession.Model(&model.User{}).Create(&user).Error
	return insertErr
}

func CheckOneUser(account string) bool {
	result := false
	var user model.User

	dbResult := dao.SqlSession.Where("account = ?", account).Find(&user)
	if dbResult.Error != nil {
		fmt.Printf("Get User Info Failed:%v\n", dbResult.Error)
	} else {
		result = true
	}
	return result
}
  • UserFields为我们SelectOneUsers()所要调用的User table中的field
  • SelectOneUsers()为利用id query User table的orm function
  • RegisterOneUser()为透过account, password以及email去创建一笔新资料在User table的orm function
  • CheckOneUser()则为检查该account是否已经存在於User table的orm function

Controller

最後则是这次新增的controller directory,这边主要是用来撰写API逻辑的地方,举例来说一次的GET DATA API,我们会将其切割为routing, business logic, orm query三大部分,controller这层就是用来实现business logic的地方

controller/user.go

package controller

import (
	"github.com/gin-gonic/gin"
	"ironman-2021/app/service"
	"net/http"
	"strconv"
)

type UsersController struct {}

func NewUsersController() UsersController {
	return UsersController{}
}

func QueryUsersController() UsersController {
	return UsersController{}
}

type Register struct {
	Account string `json:"account" binding:"required" example:"account"`
	Password string `json:"password" binding:"required" example:"password"`
	Email string `json:"email" binding:"required" example:"[email protected]"`
}

func (u UsersController) CreateUser (c *gin.Context){
	var form Register
	bindErr := c.BindJSON(&form)

	if bindErr == nil {
		err := service.RegisterOneUser(form.Account, form.Password, form.Email)
		if err == nil {
			c.JSON(http.StatusOK, gin.H{
				"status": 1,
				"msg": "success Register",
				"data": nil,
			})
		} else {
			c.JSON(http.StatusInternalServerError, gin.H{
				"status": -1,
				"msg": "Register Failed" + err.Error(),
				"data": nil,
			})
		}
	} else {
		c.JSON(http.StatusBadRequest, gin.H{
			"status": -1,
			"msg": "Failed to parse register data" + bindErr.Error(),
			"data": nil,
		})
	}
}

func (u UsersController) GetUser (c *gin.Context){
	id := c.Params.ByName("id")

	userId, err := strconv.ParseInt(id, 10, 64)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"status": -1,
			"msg": "Failed to parse params" + err.Error(),
			"data": nil,
		})
	}

	userOne, err := service.SelectOneUsers(userId)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{
			"status": -1,
			"msg": "User not found" + err.Error(),
			"data": nil,
		})
	} else {
		c.JSON(http.StatusOK, gin.H{
			"status": 0,
			"msg":  "Successfully get user data",
			"user": &userOne,
		})
	}
}

由於这次范例并没写什麽商业逻辑,就只做简单的call service function进行data的create与query而已,故不在赘述!

Summary

这章节我们从分割程序码结构、如何使用使用orm并串接上API做了一次范例给大家看,这次的code也会放在以下连结,有兴趣者可以参考!

https://github.com/Neskem/Ironman-2021/tree/Day-18


<<:  17. 当mentor比你想像的重要多

>>:  Day 18: LeetCode 322. Coin Change

Day 13: Monitor and Log with Google Cloud Operations Suite: Challenge Lab

Tasks: Initialize Cloud Monitoring. Navigation men...

Day24 :【TypeScript 学起来】TypeScript 中使用 Class

今天来了解在 TypeScript 中使用 Class,Class Member 包含了: Fie...

Football Betting - Making Sense of the Odds

Football Betting - Making Sense of the Odds Footba...

Day 27 - 上传档案 Carrierwave - 多个档案

昨天说的是单一档案上传,如果要多个档案上传的话... 建立新栏位 资料表先新增可以储存一个阵列的栏位...

第28天-CSS-影像-(3-2)

重复影像 background-repeat 这个属性可以重复图像在背景 如果是使用小图做素材 可以...