Skip to content

Latest commit

 

History

History
608 lines (406 loc) · 19.1 KB

04 用户模块-模型相关功能开发.md

File metadata and controls

608 lines (406 loc) · 19.1 KB

使用rails6 开发纯后端 API 项目

用户模块: 模型


本节目标
  • 编写用户表相关的迁移文件并使用Rails提供的命令创建用户表;
  • 编写用户模型的验证;
  • 编写用户模型的单元测试,完成相关测试。

今天我们先从用户模块的开发走起。

我们约定:在我们的系统中,用户使用自己的邮箱和密码进行登录和注册。基于这个前提我们来开发我们的用户模块。今天我们先来进行用户模型相关功能的开发,在下一节我们将完成控制器相关功能及对外提供 API 来允许用户使用 路由直接进行各种操作。

1. 功能分析

1.1 用户需要具有的字段
  • 用户需要邮箱注册,所以需要邮箱字段;
  • 用户需要密码进行登录,所以需要密码字段;
  • 用户拥有不同的权限,在我们的系统中,权限的分类比较简单,我们可以定义一个 角色 字段来实现;
  • 每个用户都应该由唯一的id,Rails已经为我们提供了默认的id字段,并且是主键,所以我们在这里不用自定义,直接使用默认即可;
  • Rails还会自动帮我们维护两个字段:created_at, updated_at 。
1.2 用户表:users
字段 类型 长度 注释 null 默认值
id unsigned integer 11 主键id,自动增长 0
email string 100 邮箱 空字符串
password_digest string 256 密码 空字符串
role unsigned tiny_int 2 角色:0-管理员 1-普通用户 2-店家 1
created_at timestamp --- 创建时间 当前时间
updated_at timestamp --- 修改时间 当前时间
1.3 要验证的字段
  • 用户注册和登录:email字段信息必须提供
  • 用户注册和登录:password_digest 字段信息必须提供
  • 角色只能包含:0-管理员 1-普通用户 2-店家

2. 模型相关功能开发

2.1 知识点预热

2.1.1 Rails 相关
  • 使用命令创建模型

    $ rails generate model 单数模型名 字段1:字段类型  字段2:字段类型

    该命令会生成迁移文件,模型,以及模型的测试文件。

  • Rails中提供的字段类型和常用数据库字段类型:

*Rails* *mysql* *postgresql* *sqlite*
:binary blob bytea blob
:boolean tinyint(1) boolean boolean
:date date date date
:datetime datetime timestamp datetime
:decimal decimal decimal decimal
:float float float float
:integer int(11) integer integer
:string varchar(255) * varchar(255)
:text text text text
:time time time datetime
:timestamp datetime timestamp datetime
  • 创建数据表迁移的编写语法

    class CreateUsers < ActiveRecord::Migration[6.1]
      def change
        create_table :复数表名 do |t|
          t.字段类型 :字段名称, limit:100, null:false, default:''
    	  ... ...
          t.timestamps
        end
      end
    end
  • 执行迁移的命令

    $ rails db:migrate
  • 模型中字段内置验证的基本语法

    validates :字段, 验证对规则:{具体的验证规则, message:"错误信息"}
  • 模型创建新对象并保存

    模型.new(字段1:, ...).save
    或者
    模型.create(字段1:, ...)
  • 模型单元测试的基本语法

    test '测试描述:不允许重复' do
        模型对象 = 模型.new(字段1:, ...)
        # assert表示断言通过验证 
        assert 模型对象.valid?
        # assert_not 表示断言没有通过验证 
        assert_not 模型对象.valid?
    end

2.2 文件版本管理

2.2.1 切换分支到 chapter04
$ git checkout -b chapter04

2.3 用户模型迁移编写

2.3.1 使用命令创建用户模型迁移文件
$ rails generate model User email:string password_digest:string role:integer
Running via Spring preloader in process 5041
      invoke  active_record
      create    db/migrate/20210507090011_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml

可以看到命令行帮我们生成了 db/migrate/20210507090011_create_users.rb 用户模型的迁移文件。

还帮我们生成了 app/models/user.rb 用户模型文件。

2.3.2 编辑用户模型的迁移文件

db/migrate/20210507090011_create_users.rb

class CreateUsers < ActiveRecord::Migration[6.1]
  def change
    create_table :users do |t|
      t.string :email, limit:100, null:false, default:''
      t.index :email, unique: true
      t.string :password_digest, limit:256, null:false, default:''
      t.integer :role, default: 1, null: false, unsigned: true

      t.timestamps
    end
  end
end

我们这里创建了users表,为email字段添加了唯一索引

2.3.3 执行迁移命令
$ rails db:migrate                                                          
== 20210507090011 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0154s
== 20210507090011 CreateUsers: migrated (0.0155s) =============================

执行结果显示创建了 users 表,在rails中,我们的模型名使用英语单词单数,生成的表名对应了英语单词的复数形式。

2.4 为User模型添加验证

2.4.1 修改模型文件,添加字段验证

思路分析

我们需要做的验证:

  • email:不能为空,不能重复, 必须符合格式;

  • password_digest:不能为空;

  • role:值必须是在 [0, 1, 2] 中的一个。


app/models/user.rb

class User < ApplicationRecord
	validates :email, presence: true,
                    uniqueness: true,
                    format: { with: /\w+@\w+\.{1}[a-zA-Z]{2,}/ }
	validates :password_digest, presence: true
	validates :role,  inclusion: { in: [0, 1, 2], message:"role can be only in [0 1 2]" }
end
2.4.2 在 Rails console 中简单测试
$ rails console
2.7.2 :001 > u = User.new({email:"testemail", password_digest:'', role:5})
2.7.2 :002 > u.valid?
2.7.2 :003 > u.errors.messages
 => {:email=>["is invalid"], :password_digest=>["can't be blank"], :role=>["role can be only in [0 1 2]"]} 

通过错误信息,可知三种验证都未通过

2.5 为模型验证编写单元测试


*基本思路分析

我们使用Rails内置的 Minitest 测试框架来编写以及测试我们的应用。

对于单元测试的编写,我们需要进行两种基本类型场景测试:成功的场景和失败的场景。而模型的验证,在这里主要是针对模型中对字段的相关 validates 的针对性测试!

  • 成功的场景
    • 使用全部合法的参数(合法的email,合法的password_digest,合法的role)创建用户,断言:通过验证
  • 失败的场景
    • 使用 非法的email,合法的password_digest,合法的role创建用户,断言:未通过验证
    • 使用 重复的email,合法的password_digest,合法的role创建用户,断言:未通过验证
    • 使用 非法的password_digest,合法的email,合法的role创建用户,断言:未通过验证
    • 使用 非法的role,合法的email,合法的password_digest创建用户,断言:未通过验证

2.5.1 准备测试用用户预定义数据

在使用Rails的命令创建模型user时,Rails还帮我们自动创建了测试文件已经与模型对应的测试用预定义数据文件:

test/fixtures/users.yml

我们可以修改这个文件的内容如下:

one:
  email: 'user1@demo.com'
  password_digest: '123456'
  role: 1

two:
  email: 'user2@demo.com'
  password_digest: '123456'
  role: 1

这里定义了两个用户:用户one 和 用户 two, 然后我们就可以在测试文件中通过 users(:one)来获取第一个用户的对象, users(:two)来获取第二个用户的对象信息了。

2.5.2 编写测试

测试的编写我们首先要确定文件路径:test/models/user_test.rb, 如果你仔细观察,这个文件也是Rails帮我们自动创建的!现在开始编写相关测试吧!

  • 使用全部合法的参数(合法的email,合法的password_digest,合法的role)创建用户,断言:通过验证

    test/models/user_test.rb

    class UserTest < ActiveSupport::TestCase
      # 使用合法参数
      test 'valid: user with all valid things' do
        user = User.new(email: 'user0@demo.com', password_digest:'123456', role:1)
        assert user.valid?
      end
      #...
    end
  • 使用 非法的email,合法的password_digest,合法的role创建用户,断言:未通过验证

    test/models/user_test.rb

    class UserTest < ActiveSupport::TestCase
      # 使用不符合格式发email
      test 'invalid: user with invalid email' do
        user = User.new(email: 'test', password_digest:'123456', role:1)
        assert_not user.valid?
      end
    end
  • 使用 重复的email,合法的password_digest,合法的role创建用户,断言:未通过验证

    test/models/user_test.rb

    class UserTest < ActiveSupport::TestCase
      # 重复的邮箱
      test 'invalid: user with taken email' do
        user = User.new(email: users(:one).email, password_digest:'123456', role:1)
        assert_not user.valid?
      end
    end
  • 使用 非法的password_digest,合法的email,合法的role创建用户,断言:未通过验证

    test/models/user_test.rb

    class UserTest < ActiveSupport::TestCase
      # 使用不合法的password_digest
      test 'invalid: user with invalid password_digest' do
        user = User.new(email: 'test1@test.cn', password_digest:'', role:1)
        assert_not user.valid?
      end
    end
  • 使用 非法的role,合法的email,合法的password_digest创建用户,断言:未通过验证

    test/models/user_test.rb

    class UserTest < ActiveSupport::TestCase
      # 使用不合法的role
      test 'invalid: user with invalid role' do
        user = User.new(email: 'test2@test.cn', password_digest:'123456', role:5)
        assert_not user.valid?
      end
    end
2.5.3 运行测试
$ rails test
Running via Spring preloader in process 7692
Run options: --seed 35082
# Running:
.....
Finished in 0.203476s, 24.5729 runs/s, 24.5729 assertions/s.
5 runs, 5 assertions, 0 failures, 0 errors, 0 skips

我们这里顺利通过测试。不过在你自己编写代码的过程中可能会遇到各种各样的问题!别着急,慢慢调试,这种麻烦正是你积累经验的好帮手!

不过到目前为止,我们的密码保存的还都是明文,在实际的工作项目中,这是不允许的。下面我们将利用 Rails 内置的加密功能来实现密码的加密保存!

2.6 密码加密


*基本思路分析

其实Rails框架中的ActiveModel::SecurePassword::has_secure_password已经为我们提供了密码加密、验证等一系列功能。我们在这里要使用它,不过我们最好先弄明白原理,所以我们可以尝试阅读源码!为了更方便阅读,我把源码做了小小的整理,下面是我经过我整理过的源码:

module ActiveModel
  module SecurePassword
    extend ActiveSupport::Concern
    module ClassMethods
      # 安全密码
      def has_secure_password(attribute = :password, validations: true)
        # 引入"bcrypt"
        require "bcrypt"
        # password=
        define_method("#{attribute}=") do |unencrypted_password|
          # 设定password为未加密的密码
          instance_variable_set("@#{attribute}", unencrypted_password)
          cost = 12
          # 设定password_digest为加密的密码
          self.public_send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
        end
        # password_confirmation=
        define_method("#{attribute}_confirmation=") do |unencrypted_password|
          # 设定password_confirmation为未加密的密码
          instance_variable_set("@#{attribute}_confirmation", unencrypted_password)
        end
        # authenticate_password
        define_method("authenticate_#{attribute}") do |unencrypted_password|
          attribute_digest = public_send("#{attribute}_digest")
          # 验证密码 是否正确
          BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self
        end
        # authenticate 别名
        alias_method :authenticate, :authenticate_password if attribute == :password

        # 默认就是true 需要验证
        if validations
          include ActiveModel::Validations

          validate do |record|
            # 创建用户 password 不能为空
            record.errors.add(attribute, :blank) unless record.public_send("#{attribute}_digest").present?
          end
          # password 最长72个字符
          validates_length_of attribute, maximum: 72
          # 如果存在就验证 password_confirmation == password
          validates_confirmation_of attribute, allow_blank: true
        end
      end
    end
  end
end

通过以上源码,我们可以确认几条对我们比较重要的信息:

  • has_secure_password方法接受的参数中密码的字段默认名称是:password 而不是 password_digest;
  • has_secure_password方法严重依赖外部的 bcrypt;
  • has_secure_password方法把加密后的密码保存到了 password_digest ,这也是为什么users表中密码字段名称为 password_digest;
  • has_secure_password方法添加了以下几条password字段的验证
    • 创建用户时password不能为空;
    • password最长不能超过72个字符;
    • 如果传递了 attribute_confirmation字段,会进行两次密码相同的比较,没传则不比较。

基于以上结论,我们可以确定我们需要完成的步骤:

  • 首先引入 bcrypt
  • 在user模型中调用 has_secure_password方法。

2.6.1 引入 bcrypt

bcrypt是一个gem, 我们可以使用命令 bundle add bcrypt 安装, 不过其实我们的项目已经默认添加了 bcrypt,只不过是注释状态,我们可以打开 Gemfile文件,去掉bcrypt前面的注释:

# Use Active Model has_secure_password
gem 'bcrypt', '~> 3.1.7'
# ...

然后在项目根目录下执行命令安装新增的gem

$ bundle

现在就可以使用 bcrypt了。

2.6.2 在user模型中调用 has_secure_password方法

has_secure_passwordActiveModel的类方法, 继承自 ActiveModel类的子类都可以直接调用,Rails模型都是 ActiveModel的子类,所以我们可以直接在users中直接调用即可,参数使用 has_secure_password 默认参数!

app/models/user.rb

class User < ApplicationRecord
  # ...
  has_secure_password
end

有了 has_secure_password 方法的加持,对于 User 模型,要创建用户我们需要提供的字段包括:emailpasswordrole, 即:

User.new({email:"a@a.com", password:'123456', role:1})

password_digest字段由has_secure_password方法来维护,而且password_digest的值是加密后的密码。

2.6.3 在Rails console 中调用 测试
$ rails console
2.7.2 :001 > u = User.new({email:"a@a.com", password:'123456', role:1})
2.7.2 :002 > u.save
  TRANSACTION (0.8ms)  BEGIN
  User Exists? (34.6ms)  SELECT 1 AS one FROM `users` WHERE `users`.`email` = 'a@a.com' LIMIT 1
  User Create (1.7ms)  INSERT INTO `users` (`email`, `password_digest`, `created_at`, `updated_at`) VALUES ('a@a.com', '$2a$12$Bn6pEc/pyeWpmKwM0nfIF.0ZSK41W4H3Qc96aq.Sg/f0XXEQo6P7y', '2021-05-08 01:56:28.110434', '2021-05-08 01:56:28.110434')
  TRANSACTION (2.8ms)  COMMIT
 => true 
2.7.2 :003 > u
 => #<User id: 2, email: "a@a.com", password_digest: [FILTERED], role: 1, created_at: "2021-05-08 01:56:28.110434000 +0000", updated_at: "2021-05-08 01:56:28.110434000 +0000"> 

可以观察到密码被加密了并且被保存到了 users 表的 password_digest 字段中。

到这里,模型相关的开发工作,可以告一段落了。

2.6.4 为password新增测试
  • 使用 合法的password,合法的email,合法的role创建用户,断言:通过验证

    test/models/user_test.rb

    class UserTest < ActiveSupport::TestCase
      # 使用合法的password
      test 'valid: user with valid password' do
        user = User.new(email: 'test3@test.cn', password:'123456', role:1)
        assert user.valid?
      end
    end
  • 使用 非法的password,合法的email,合法的role创建用户,断言:未通过验证

    test/models/user_test.rb

    class UserTest < ActiveSupport::TestCase
      # 使用合法的password
      test 'invalid: user with invalid password' do
        user = User.new(email: 'test4@test.cn', password:'', role:1)
        assert_not user.valid?
      end
    end
2.6.5 为password运行测试
$ rails test
Running via Spring preloader in process 3990
Run options: --seed 8355
# Running:
.......
Finished in 0.307520s, 22.7627 runs/s, 22.7627 assertions/s.
7 runs, 7 assertions, 0 failures, 0 errors, 0 skips

2.7 文件版本管理

2.7.1 添加所有变动文件到git管理
$ git add .
2.7.2 提交所有变动文件
$ git commit -m "set users model with validation and bcrypt"

3. 总结

我们完成了用户模型验证和密码加密功能,下节课我们将开发用户控制器相关功能!一定要跟上脚步,认真练习!