首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >处理特定于模型的操作与处理数据库操作

处理特定于模型的操作与处理数据库操作
EN

Stack Overflow用户
提问于 2018-05-30 00:20:18
回答 1查看 77关注 0票数 0

当涉及到模型设计时,我遇到了一些问题,特别是处理特定于模型的操作数据库操作。我的用户模型就是一个很好的例子。

在我的数据库中创建用户时,我希望:

  1. 验证密码是否符合条件(模型操作)
  2. 创建摘要(模型操作)
  3. 设置时间戳(模型操作)
  4. 将电子邮件、摘要和时间戳保存到数据库(数据库操作)

在测试时,我显然希望对所有4个都进行一组单元测试,但是#4调用了其他3个,这是我不想重新测试的,否则如果这3个调用中的任何一个都会有#4测试失败的风险。

我已经想出了为ModelActions和StoreActions创建一个单独的接口,并在需要时将UserAction接口发送到存储操作,然而,当我写出它时,我已经感觉到了一些严重的代码味道。

代码语言:javascript
复制
type User struct {
        ID                   int    `json:"id"`
        Email                string `json:"email"`
        Password             string `json:"password"`
        ConfirmationPassword string `json:"confirmationPassword"`
        passwordDigest       string `json:"-"`
        CreatedAt            time.Time `json:"createdAt,omitempty"`
        ModifiedAt           time.Time `json:"modifiedAt,omitempty"`
}

//UserStore is the interface for all User functions that interact with the database
type UserStore interface {
        GetUserByEmailAndPassword(email, password string) (User, error)
        UpdatePassword(u UserAction, previousPassword, password, confirmationPassword string) error
        UserExists(email string) (bool, error)
        CreateUser(u UserAction) error
}

// I am going against design Principles by having GetID, GetEmail, since JSON unmarshalling needs the struct fields to be capitalized, which is already a warning sign for me

type UserAction interface {
        GetID() int
        GetEmail() string
        Timestamps() (time.Time, time.Time)
        SetID(id int)
        SetTimestamps()
        SetPassword(password, confirmation string)
        SetDigest(digest string)
        CreateDigest() (string, error)
        VerifyPassword() error
        ComparePassword(password string) error
}

// Example of UserActions
func (u *User) CreateDigest() (string, error) {
        var digest string
        if err := u.VerifyPassword(); err != nil {
                return digest, err
        }

        passwordByte, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
        if err != nil {
                return digest, err
        }

        digest = string(passwordByte)
        return digest, nil
}

func (u *User) VerifyPassword() error {
        if len(u.Password) < 6 {
                return &modelError{"Password", "must be at least 6 characters long"}
        }

        if u.Password != u.ConfirmationPassword {
                return &modelError{"ConfirmationPassword", "does not match Password"}
        }

        return nil
}

// Example of DB Action
func (db *DB) CreateUser(ua UserAction) error {
        if exists, err := db.UserExists(ua.GetEmail()); err != nil {
                return err
        } else if exists {
                return &modelError{"Email", "already exists in the system"}
        }

        // set password
        digest, err := ua.CreateDigest()
        if  err != nil {
                return err
        }

        ua.SetDigest(digest)
        ua.SetTimestamps()

        createdAt, modifiedAt := ua.Timestamps()

        rows, err := db.Query(`
                INSERT INTO users (email, password_digest, created_at, modified_at)
                        VALUES ($1, $2, $3, $4)
                        RETURNING id
                `, ua.GetEmail(), digest, createdAt, modifiedAt)

        if err != nil {
                return err
        }

        defer rows.Close()

        var id int
        for rows.Next() {
                if err := rows.Scan(&id); err != nil {
                        return err
                }
        }

        ua.SetID(id)

        return nil
}

有没有一种更好的方法来对这些独立的操作建模,以便在测试DB/Store函数时可以模拟UserActions?我尝试将User结构存储为接口的一部分,例如:

代码语言:javascript
复制
type UserAction {
    SetTimestamps()
    CreateDigest() (string, error)
    VerifyPassword() error
    ComparePassword(password string) error
    User() *User
}

然而,这会在创建模拟时导致循环导入,还会打开所有字段,因为模型的字段是可导出的,因此可以说这些字段已经可用

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2018-05-31 08:55:21

我认为你的用户应该是一个具体的类型,你应该使用一个界面来模拟你的商店。

例如,这样的项目结构对我来说很有意义:

代码语言:javascript
复制
cmd/
   server/
      user.go
      user_test.go
      main.go
      store.go
mysql/
   mysql.go
   user.go
   user_test.go
user.go
user_test.go

您的用户模型位于根目录user.go中。该文件将包含您的User结构,以及将在其上操作的函数,如CreateDigest。这些函数应该在user_test.go中进行测试。

值得一提的是,在你的根目录下,你的包不应该是main,你的包的名称应该是你的项目的名称,我们将它称为myapp

您的mysqlpostgres等也应该是一个具体的实现。您可能在该包中有一个函数,如:

代码语言:javascript
复制
func (m *MySQL) InsertUser(u *myapp.User) error

此函数应在mysql/user_test.go中测试。

最后,我们可以将它们放在server中。这是您实际部署或运行的二进制文件。

cmd/server/store.go中,您应该创建一个将由mysql实现的接口。

cmd/server/user_test.go中,可以很容易地模拟这一点,这样您就不必访问真正的数据库。我相信你的界面应该存在于你的客户端中。在本例中,servermysql的客户端。

cmd/server/user.go中,您可能具有如下所示的函数:

代码语言:javascript
复制
func CreateUser(w http.ResponseWriter, r *http.Request) {
  var u myapp.user
  err := json.NewDecoder(r.Body).Decode(&u)
  if err != nil {
    panic(err) // don't do this for real
  }

  d := myapp.CreateDigest(u.Password)
  u.Digest = d

  // s is the interface, defined in `cmd/server/store.go`, but is implemented by mysql
  err = s.InsertUser(&u)
  if err != nil {
    panic(err)
  }

  // Since we pass a pointer, you can have your store set the ID of the user
  fmt.Println(u.ID)
}

现在您有了更好的关注点分离,一切都应该很容易测试,并且对现有代码进行更改也很容易。

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/50588831

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档