【山竹记账后端】2.RESTful API 与路由、分页实现
大纲链接 §
[toc]
1. REST风格是什么 ⇧
REST是什么?
Representational State Transfer- 一种网络软件 架构风格
- 不是标准、不是协议、不是接口,只是一种风格
- Roy于2000年在自己博士论文中提到此术语
- Roy曾参与撰写HTTP规格文档
怎么做?
- 以资源为中心,一旦此为名字一般就是资源,如果为动词就不是资源:用户、创建用户
- 充分利用 HTTP现有功能,如动词、状态码、头部字段
- Github API 就比较符合
REST
参考
REST风格举例
请求1:创建 item
|
|
- 注意货币的最小单位,避免浮点数的精度问题,
99分
请求2:创建 item
|
|
请求3:更新 item
|
|
反风格:
|
|
- 反方观点:全用 POST 多省事儿
- 我方观点:自己想路径,多费事儿
请求4:删除 item
|
|
反风格:
|
|
- 反方观点:POST 多省事儿
- 我方观点:有 DELETE 不用非要自己想,多费事儿
请求5:获取 一个或多个 item
|
|
- 单条或多条记录
- 单条
GET /api/v1/items/1省略了id - 多条
GET /api/v1/items?page=1&per_page=10获取分页数据 - 按用户获取通过传
id获取多条GET /api/v1/user/2/items?...从属关系,资源嵌套 - 无资源嵌套,拍平
GET /api/v1/items?user_id=2
- 单条
- 数组
- 属于两个tag的记录
GET /api/v1/items?tags_id[]=1&tag_id[]=2 - 表示资源数组
tags_id[],后端框架自动拼接,比如rails - 其他框架的反风格:
GET /api/v1/items?tags_id=1,2 - 如果
url太长就不得不改为POST
- 属于两个tag的记录
- 排序,二重排序
GET /api/v1/items?sort_by[]=id+asc&sort_by[]=name+desc+就是空格- 排序条件相同则按创建时间排序
- 搜索
GET /api/v1/items?keyword=hi- 反风格
GET /api/v1/items/search/hi
REST风格总结 ⇧
- 尽量以资源为中心:
url里的items就是资源 - 尽量使用
HTTP现有功能:其实响应头里也可以包含内容,但目前的例子都没有用到 - 可以适当违反规则:比如
/api/v1/items/search/hi
人话版REST风格总结 ⇧
- 看见 路径 就知道请求什么东西
- 看见 动词 就知道是什么操作
- 看见 状态码 就知道结果是什么
200- 成功201- 创建成功400- 其他所有错误,详细原因可以放在body里404- 未找到403- 没有权限401- 未登录422- 无法处理的实体,参数有问题402- 需付费412- 不满足前提条件,流程中常用429- 请求太频繁
为什么不喜欢REST ⇧
需求:批量创建
items
POST /api/v1/items只能创建一个item返回一个结果
可以适当改造
REST
POST /api/v1/items/batch- 请求消息体
[{"amount": 1}, {"amount": -2}, {"amount": 3}] - 响应内容
{"resources: [{...}, null, {...}]"}, {"errors": [null, {...}, null]} - 更多批处理
POST /api/v1/items/batchDELETE /api/v1/items/batchUPDATE /api/v1/items/batch
符合
REST风格的API就叫RESTful API
2. API概要设计 ⇧
- 使用
Rails提供的强大工具
2.1 发送验证码 ⇧
- 资源:
validation_codes - 动作: 只有一个
createPOST - 状态码:
200 | 201 | 422 | 429即成功 | 创建成功 | 请求参数验证错误 | 请求太频繁
2.2 登入登出 ⇧
- 资源:
session(注意没有s,单点登录) - 动作:
create | destroyPOST | DELETE - 状态码:
200 | 422
2.3 当前用户 ⇧
- 资源:
me - 动作:
showGET - 状态码:
200 | 429
2.4 记账数据 ⇧
- 资源:
items - 动作:
create | update | show | index | destroyupdate对应PATCH表示部分字段更新show对应GET /items/:id用来表示一条记账记录index对应GET /items?since=2026-01-01&before=2026-02-01destroy对应DELETE表示删除,一般为软删除
- 状态码:
200 | 201 | 422 | 429
2.5 标签 ⇧
- 资源:
tags - 动作:
create | update | show | index | destroy - 状态码:
200 | 201 | 422 | 429
2.6 打标签* 暂不实现 ⇧
记录用户和标签的关系
- 资源:
taggings(动词的名词形式) - 动作:
create | index | destroy - 状态码:
200 | 201 | 422 | 429
API概要设计已完成
- 接下来有一些细节要注意
3. 开始实现-路由 api ⇧
手动添加路由 config/routes.rb ⇧
|
|
对比使用 namespaces 简略写路径,自动生成路由 ⇧
|
|
namespace自动添加前缀(命名空间)- 运行命令
bin/rails routes查看路由:URI Pattern对应Controller#ActionGET /api/v1/validation_codes(.:format)对应api/v1/validation_codes#indexPOST /api/v1/validation_codes(.:format)对应api/v1/validation_codes#createGET /api/v1/validation_codes/:id(.:format)对应api/v1/validation_codes#showPATCH /api/v1/validation_codes/:id(.:format)对应api/v1/validation_codes#updateDELETE /api/v1/validation_codes/:id(.:format)对应api/v1/validation_codes#destroy
resources :xxx自动生成资源对应的6个方法- 手动实现这些
api对应的方法
配置路由缺省方法 ⇧
|
|
resources :validation_codes, only: [:create]仅生成配置的路由、方法
配置好其他所有路由 ⇧
|
|
具体对应方法实现先暂时不写
参考文档
3. 实现 validation_codes 的 create 的路由 ⇧
创建数据表 ⇧
创建表
运行命令
bin/rails g model ValidationCode email:string kind:integer used_at:datetime注意
ValidationCode没有s1 2 3 4 5bin/rails g model ValidationCode email:string kind:integer used_at:datetime # invoke active_record # create db/migrate/20260909153656_create_validation_codes.rb # create app/models/validation_code.rb
db/migrate/20260909153656_create_validation_codes.rb修改部分配置
|
|
迁移数据库 ⇧
运行命令
bin/rails db:migrate1 2 3 4== 20260909153656 CreateValidationCodes: migrating ============================ -- create_table(:validation_codes) -> 0.0273s == 20260909153656 CreateValidationCodes: migrated (0.0274s) ===================
反悔迁移数据库 ⇧
由于
validation_codes的code字段未添加,需要撤销迁移
运行命令
bin/rails db:rollback撤销迁移1 2 3 4== 20260909153656 CreateValidationCodes: reverting ============================ -- drop_table(:validation_codes) -> 0.0048s == 20260909153656 CreateValidationCodes: reverted (0.0082s) ===================
再次修改
db/migrate/20260909153656_create_validation_codes.rb
|
|
再次运行
bin/rails db:migrate1 2 3 4== 20260909153656 CreateValidationCodes: migrating ============================ -- create_table(:validation_codes) -> 0.0135s == 20260909153656 CreateValidationCodes: migrated (0.0136s) ===================
创建 Controller ⇧
运行命令
bin/rails g controller validation_codes create1 2create app/controllers/validation_codes_controller.rb route get "validation_codes/create"删除自动添加生成的路由
修改前缀 添加目录 api/v1 ⇧
- 在
app/controllers中添加目录api/v1 - 将生成的
validation_codes_controller.rb移到app/controllers/api/v1目录下 修改
validation_codes_controller.rb中类名,添加命名空间前缀Api::V1::ValidationCodesController注意命名空间需要添加两个冒号隔开
Api::V1::1 2 3 4 5class Api::V1::ValidationCodesController < ApplicationController def create head 201 end end
启动服务
bin/rails s查看是否可以访问到
/api/v1/validation_codes- 使用命令
curl -X POST http://127.0.0.1:3000/api/v1/validation_codes -v -v查看返回结果中状态码1< HTTP/1.1 201 Created
- 使用命令
修改
head 202再运行curl -X POST http://127.0.0.1:3000/api/v1/validation_codes -v查看返回结果中状态码
5. 实现 items 的分页 ⇧
实现分页的两种方案
- 使用
page和per_page参数,见kaminari或pagy库 - 使用
start_id和limit参数,需要id是自增数字
创建 Items 的 Controller ⇧
运行命令
bin/rails g controller Api::V1::Items index create注意有两个冒号
::是ruby用来区分命名空间层级的符号1# create app/controllers/api/v1/items_controller.rb
删除自动生成的路由
|
|
创建 Items 的 Model ⇧
- 运行命令
bin/rails g model item user_id:integer amount:integer notes:text happened_at:datetimetext和string的区别就是对应数据库中string短字符和varchar长字符
补充字段 重新运行命令
bin/rails g model item user_id:integer amount:integer notes:text tags_id:integer happened_at:datetime --force添加
--force会删除之前创建的,在重新创建当前的同名 model1 2 3 4 5 6 7 8# invoke active_record # create db/migrate/20260910062438_create_items.rb # create app/models/item.rb # invoke active_record # remove db/migrate/20260910062438_create_items.rb # create db/migrate/20260910063034_create_items.rb # identical app/models/item.rb
创建的空文件
app/models/item.rb先不管
|
|
修改创建的文件
db/migrate/20260910063034_create_items.rb中字段类型
|
|
user_id暂时不做外键- 做测试时,外键需要把相关所有表的信息都填好
- 数据库层面,暂时只需要做指引就好了
tags_id为数组,需要加上array: truePostgrsSql支持数组类型
数据迁移
运行命令
bin/rails db:migrate1 2 3 4== 20260910063034 CreateItems: migrating ====================================== -- create_table(:items) -> 0.0212s == 20260910063034 CreateItems: migrated (0.0213s) =============================右键刷新数据库插件
mangosteen_dev查看是否添加了items表
尝试实现 Items 的 Controller ⇧
实现
app/controllers/api/v1/items_controller.rb中create方法
|
|
- 使用
curl -X POST创建:curl -X POST http://127.0.0.1:3000/api/v1/items -v- 子字符串查询 终端输入
curl -X POST,然后按上,会自动查询补全上一次相近的命令
- 子字符串查询 终端输入
查看返回
|
|
手动创建了21个(后续会在测试中写自动创建逻辑)
curl -X POST http://127.0.0.1:3000/api/v1/items -v实现app/controllers/api/v1/items_controller.rb中index方法
|
|
- 先尝试打印 Item,发请求
curl GET http://127.0.0.1:3000/api/v1/items -v
修改
app/controllers/api/v1/items_controller.rb
|
|
- 使用
curl http://127.0.0.1:3000/api/v1/items -v报错< HTTP/1.1 500 Internal Server Error - 使用浏览器访问接口
http://127.0.0.1:3000/api/v1/items查看报错页面 Rails识别用户使用浏览器请求,返回一个html报错页面;识别用户使用curl请求则返回一个json
使用第三方库kaminari实现 Items 的 Controller ⇧
使用
page和per_page参数,使用第三方库kaminari或pagy
- 将
gem 'kaminari'写到Gemfile中 - 或者直接
gem install kaminari
Gemfile
|
|
- 运行命令
bundle install或者简写形式bundle安装依赖- 已默认配置好国内镜像源,加速安装依赖
- 已默认开启详细模式
bundle --verbose
- 安装成功后,重启服务
bin/rails s - 安装最新版
gem 'kaminari' - 安装特定兼容版本
gem 'kaminari', '~> 1.2.2' - 一般不用指定版本号,
Gemfile.lock自动锁版本 - 此项目依赖都安装在全局环境,项目本地目录没有依赖
- 可以使用
gem env查看INSTALLATION DIRECTORY对应的路径下 - 使用
ls /usr/local/rvm/gems/ruby-3.1.2/gems查看所有依赖 - 这个目录已在镜像中持久化
- 可以使用
参考
再次修改
app/controllers/api/v1/items_controller.rb
|
|
- 发起请求
curl GET http://127.0.0.1:3000/api/v1/items -v查看结果 kaminari默认配置每页 25 项数据,可以自定义配置- 运行命令配置
bin/rails g kaminari:config添加全局配置文件 - 自动生成
create config/initializers/kaminari_config.rb可以自行编辑配置
- 运行命令配置
kaminari初始化配置config/initializers/kaminari_config.rb
|
|
- 一般配置都放在
config/initializers/目录下 - 需要再次重启服务
bin/rails s - 发起请求
curl GET http://127.0.0.1:3000/api/v1/items -v查看结果 - 成功请求第一页的前十个数据
请求第 n 页,获取参数
params[:page],修改app/controllers/api/v1/items_controller.rb
|
|
- 使用
Item.page params[:page]获取查询参数 - 请求
curl GET http://127.0.0.1:3000/api/v1/items -v查询参数空,kaminari默认判断为 1 - 请求第二页
curl GET http://127.0.0.1:3000/api/v1/items\?page\=2 -v- 注意命令行中会自动添加转译符号反斜杠
\
- 注意命令行中会自动添加转译符号反斜杠
- 请求第三页
curl GET http://127.0.0.1:3000/api/v1/items\?page\=3 -v - 超过允许最大页码返回
{"resource":[]} - 自定义每页100个
item = Item.page(params[:page]).per(100)params[:page]查询参数空,或者不合法的值,kaminari默认判断为 1
分页获取总条数,修改
app/controllers/api/v1/items_controller.rb
|
|
rails适合不需要考虑太多细节,比如性能问题,优先思考清楚后端的流程、步骤
参考
对比使用 pagy 分页 ⇧
kaminari的性能堪忧,虽然写法较pagy友好,更傻瓜式,容易理解上手,本项目还是使用此库
- 注重性能可以以后更换此库
参考
区别于 社交媒体网站所采用的分页流派:信息流(Feed 流)分页 ⇧
区别于 例如传统博客页面使用
page和per_page参数,见kaminari或pagy库 信息流(Feed 流)分页 使用start_id和limit参数,需要id是自增数字,或者时间戳(用start_time)
- 下拉更新页面,永远都是获取第一页
- 第一页数据变更特别快,下拉后原来的第一页可能已经变为 第1000页了
- 最新下拉获取 使用
start_id的最新一条,一次拉取limit条 - 无需任何库实现,直接写查询语句
item = Item.where(""id > ?", params[:start_id]).limit(20)
6. $3 ⇧
7. $3 ⇧
8. $3 ⇧
参考文章
相关文章
- 无
- 作者: Joel
- 文章链接:
- 版权声明
- 非自由转载-非商用-非衍生-保持署名