• 05-Database
    • 数据库及ORM选择
    • 数据库模型
    • 初始化数据库数据
    • 完善view
    • Links

    05-Database

    本章的主题是重中之重!大多数应用都需要持久化存储数据,并高效地执行的增删查改的操作,数据库 为此而生。

    我们将第一次引入第三方库 Gorm 来帮助我们实现 ORM

    本章的GitHub链接为: Source, Diff,
    Zip

    数据库及ORM选择

    数据库被划分为两大类,遵循关系模型的一类是 关系数据库,另外的则是 非关系数据库,简称 NoSQL,表现在它们不支持流行的关系查询语言SQL。这里我们打算使用的是 mysql, 这是一种非常流行的关系型数据库,也应该是应用最广泛的数据库了。

    ORM我们选用Gorm, 应该是因为它的Star数遥遥领先吧。

    首先我们利用下面的命令安装 Gorm

    1. go get -u github.com/jinzhu/gorm

    数据库模型

    定义数据库中一张表及其字段的类,通常叫做数据模型。ORM (Gorm) 会将类的实例关联到数据库表中的数据行,并翻译相关操作

    05-Database - 图1

    id字段通常存在于所有模型并用作主键。每个用户都会被数据库分配一个id值,并存储到这个字段中。大多数情况下,主键都是数据库自动赋值的,我只需要提供id字段作为主键即可。

    • user表: username,email和password_hash字段被定义为字符串(数据库术语中的VARCHAR),并指定其最大长度,以便数据库可以优化空间使用率。 username和email字段的用途不言而喻,password_hash字段值得提一下。 我想确保我正在构建的应用采用安全最佳实践,因此我不会将用户密码明文存储在数据库中。 明文存储密码的问题是,如果数据库被攻破,攻击者就会获得密码,这对用户隐私来说可能是毁灭性的。 如果使用哈希密码,这就大大提高了安全性。 这将是另一章的主题,所以现在不需分心。

    • post表: 将具有必须的id、用户动态的body和timestamp字段。 除了这些预期的字段之外,我还添加了一个user_id字段,将该用户动态链接到其作者。 你已经看到所有用户都有一个唯一的id主键, 将用户动态链接到其作者的方法是添加对用户id的引用,这正是user_id字段所在的位置。 这个user_id字段被称为外键。 上面的数据库图显示了外键作为该字段和它引用的表的id字段之间的链接。 这种关系被称为一对多,因为“一个”用户写了“多”条动态。

    另外 我们后续还会有粉丝这种多对多的关系

    05-Database - 图2

    model/user.go

    1. package model
    2. // User struct
    3. type User struct {
    4. ID int `gorm:"primary_key"`
    5. Username string `gorm:"type:varchar(64)"`
    6. Email string `gorm:"type:varchar(120)"`
    7. PasswordHash string `gorm:"type:varchar(128)"`
    8. Posts []Post
    9. Followers []*User `gorm:"many2many:follower;association_jointable_foreignkey:follower_id"`
    10. }

    Notice: 此处应该是gorm:"type:varchar(64)" 当时写的Github源码有错,不过只影响建表时的varchar size,请注意

    model/post.go

    1. package model
    2. import (
    3. "time"
    4. )
    5. // Post struct
    6. type Post struct {
    7. ID int `gorm:"primary_key"`
    8. UserID int
    9. User User
    10. Body string `gorm:"type:varchar(180)"`
    11. Timestamp *time.Time `sql:"DEFAULT:current_timestamp"`
    12. }

    model/g.go

    1. package model
    2. import (
    3. "log"
    4. "github.com/bonfy/go-mega-code/config"
    5. "github.com/jinzhu/gorm"
    6. )
    7. var db *gorm.DB
    8. // SetDB func
    9. func SetDB(database *gorm.DB) {
    10. db = database
    11. }
    12. // ConnectToDB func
    13. func ConnectToDB() *gorm.DB {
    14. connectingStr := config.GetMysqlConnectingString()
    15. log.Println("Connet to db...")
    16. db, err := gorm.Open("mysql", connectingStr)
    17. if err != nil {
    18. panic("Failed to connect database")
    19. }
    20. db.SingularTable(true)
    21. return db
    22. }

    其实如果作为演示项目,我们可以将connectingStr := config.GetMysqlConnectingString() 直接写入实际的演示数据库地址,不过为了追求和实际项目中的将配置文件单独放置的效果,我们还是将实际项目中的方法演示给大家。

    多引入一个第三方package github.com/spf13/viper

    1. go get -u github.com/spf13/viper

    设置 config 文件, config.yml, 可以放在项目目录下 (实际 Source Code 中 git ignore 了实际的 config.yml, 存放了 config.yml.sample)

    config.yml

    1. mysql:
    2. charset: utf8
    3. db: go-mega
    4. host: localhost
    5. password: password
    6. user: root

    config/g.go

    1. package config
    2. import (
    3. "fmt"
    4. "github.com/spf13/viper"
    5. )
    6. func init() {
    7. projectName := "go-mega"
    8. getConfig(projectName)
    9. }
    10. func getConfig(projectName string) {
    11. viper.SetConfigName("config") // name of config file (without extension)
    12. viper.AddConfigPath(".") // optionally look for config in the working directory
    13. viper.AddConfigPath(fmt.Sprintf("$HOME/.%s", projectName)) // call multiple times to add many search paths
    14. viper.AddConfigPath(fmt.Sprintf("/data/docker/config/%s", projectName)) // path to look for the config file in
    15. err := viper.ReadInConfig() // Find and read the config file
    16. if err != nil { // Handle errors reading the config file
    17. panic(fmt.Errorf("Fatal error config file: %s", err))
    18. }
    19. }
    20. // GetMysqlConnectingString func
    21. func GetMysqlConnectingString() string {
    22. usr := viper.GetString("mysql.user")
    23. pwd := viper.GetString("mysql.password")
    24. host := viper.GetString("mysql.host")
    25. db := viper.GetString("mysql.db")
    26. charset := viper.GetString("mysql.charset")
    27. return fmt.Sprintf("%s:%s@tcp(%s:3306)/%s?charset=%s&parseTime=true", usr, pwd, host, db, charset)
    28. }

    由于 Go 是编译型语言,我们不能像 Python 那样可以逐行的调试演示,我们就直接编写个程序,初始化表

    cmd/db_init/main.go

    1. package main
    2. import (
    3. "log"
    4. "github.com/bonfy/go-mega-code/model"
    5. _ "github.com/jinzhu/gorm/dialects/mysql"
    6. )
    7. func main() {
    8. log.Println("DB Init ...")
    9. db := model.ConnectToDB()
    10. defer db.Close()
    11. model.SetDB(db)
    12. db.DropTableIfExists(model.User{}, model.Post{})
    13. db.CreateTable(model.User{}, model.Post{})
    14. }

    我们先在mysql中创建 go-mega 的数据库

    1. $ mysql -u root -p
    2. 输入密码后
    3. mysql> create DATABASE go-mega;

    然后运行

    1. go run cmd/db_init/main.go

    tables

    本小节 Diff

    初始化数据库数据

    model/utils.go

    1. package model
    2. import (
    3. "crypto/md5"
    4. "encoding/hex"
    5. )
    6. // GeneratePasswordHash : Use MD5
    7. func GeneratePasswordHash(pwd string) string {
    8. hasher := md5.New()
    9. hasher.Write([]byte(pwd))
    10. pwdHash := hex.EncodeToString(hasher.Sum(nil))
    11. return pwdHash
    12. }

    model/user.go

    1. ...
    2. // SetPassword func: Set PasswordHash
    3. func (u *User) SetPassword(password string) {
    4. u.PasswordHash = GeneratePasswordHash(password)
    5. }
    6. // CheckPassword func
    7. func (u *User) CheckPassword(password string) bool {
    8. return GeneratePasswordHash(password) == u.PasswordHash
    9. }
    10. // GetUserByUsername func
    11. func GetUserByUsername(username string) (*User, error) {
    12. var user User
    13. if err := db.Where("username=?", username).Find(&user).Error; err != nil {
    14. return nil, err
    15. }
    16. return &user, nil
    17. }

    model/post.go

    1. // GetPostsByUserID func
    2. func GetPostsByUserID(id int) (*[]Post, error) {
    3. var posts []Post
    4. if err := db.Preload("User").Where("user_id=?", id).Find(&posts).Error; err != nil {
    5. return nil, err
    6. }
    7. return &posts, nil
    8. }

    Notice: 特别注意这里的 Preload, 相当于预先的 Join Table,不然取得的 posts 就没有 User 信息

    cmd/db_init/main.go

    1. package main
    2. import (
    3. "log"
    4. "github.com/bonfy/go-mega-code/model"
    5. _ "github.com/jinzhu/gorm/dialects/mysql"
    6. )
    7. func main() {
    8. log.Println("DB Init ...")
    9. db := model.ConnectToDB()
    10. defer db.Close()
    11. model.SetDB(db)
    12. db.DropTableIfExists(model.User{}, model.Post{})
    13. db.CreateTable(model.User{}, model.Post{})
    14. users := []model.User{
    15. {
    16. Username: "bonfy",
    17. PasswordHash: model.GeneratePasswordHash("abc123"),
    18. Posts: []model.Post{
    19. {Body: "Beautiful day in Portland!"},
    20. },
    21. },
    22. {
    23. Username: "rene",
    24. PasswordHash: model.GeneratePasswordHash("abc123"),
    25. Email: "rene@test.com",
    26. Posts: []model.Post{
    27. {Body: "The Avengers movie was so cool!"},
    28. {Body: "Sun shine is beautiful"},
    29. },
    30. },
    31. }
    32. for _, u := range users {
    33. db.Debug().Create(&u)
    34. }
    35. }

    运行

    1. $ go run cmd/db_init/main.go

    user表
    05-Database - 图4

    post表
    05-Database - 图5

    本小节 Diff

    完善view

    现在我们的数据库里有了数据,我们就可以直接从数据库中取得数据,生成 index 的 viewmodel 了

    修改 vm/index.go 的 GetVM func, 从数据库中获得数据

    vm/index.go

    1. ...
    2. // GetVM func
    3. func (IndexViewModelOp) GetVM() IndexViewModel {
    4. u1, _ := model.GetUserByUsername("rene")
    5. posts, _ := model.GetPostsByUserID(u1.ID)
    6. v := IndexViewModel{BaseViewModel{Title: "Homepage"}, *u1, *posts}
    7. return v
    8. }

    main.go

    1. package main
    2. import (
    3. "net/http"
    4. "github.com/bonfy/go-mega-code/controller"
    5. "github.com/bonfy/go-mega-code/model"
    6. _ "github.com/jinzhu/gorm/dialects/mysql"
    7. )
    8. func main() {
    9. // Setup DB
    10. db := model.ConnectToDB()
    11. defer db.Close()
    12. model.SetDB(db)
    13. // Setup Controller
    14. controller.Startup()
    15. http.ListenAndServe(":8888", nil)
    16. }

    main.go 要初始化数据库连接,不然会报错。

    另外 import _ "github.com/jinzhu/gorm/dialects/mysql" 确定你要使用的是 mysql 的 dialect

    1. go run main.go

    05-Database - 图6

    本小节 Diff

    Notice: 由于 Gorm 的 auto migration 并不像 SQLAlchemy 那么强大,我们在使用的时候要十分小心,这里我就直接没有使用(一般项目会预先定义好数据结构,而且后期修改也可以通过SQL操作),如果有兴趣的同学可以去看下 Gorm 的文档,还是有部分的 Migration 的能力的。

    • 目录
    • 上一节: 04-Web-Form
    • 下一节: 06-User-Login