Ruby

Rails 学习杂记一

2021-10-21  本文已影响0人  YM雨蒙

系统学习 rails

Github项目地址

MVC

目录

Controller

安装 bootstrap

Model

创建 User Model

rails g model user
class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :username
      t.string :password
      # 自动维护 时间戳 创建 & 更新
      t.timestamps
    end
  end
end

bin/rails db:migrate

rails c

# User

# 创建一条 user
User.create username: 'yym', password: 123456
# 查询所有
User.all
# 查询第一条
User.first

# 更新
user = User.first
user.username = 'yym_2'
user.save

# 删除
user.destroy

routes

users
method | path | action

列表

表单相关


单一

Session & Cookie

  1. 先了解 HTTP -> 短连接 无状态

  2. http 无状态, 怎么保存状态的呢? -> Cookies

    • http response 会 Set-Cookies
    • 前端发送请求可以 cookie: 后端返回的值
  3. session 和 cookie 的区别?

    • Session是后端实现 -> session 的 key 存储基于 cookie, 也可以基于后端存储, 比如数据库
    • Cookie是浏览器的实现
    • Session 支持存储后端的数据结构, 比如对象; Cookie只支持字符串
    • session没有大小限制; cookie 大小有限制
  4. session 操作

    • session 方法; session key; session 加密的
  5. cookie 操作

    • cookie 方法; cookie 有效期, 域名等信息

Controller Filter


我们希望 看到用户列表, 需要登录

before_action :auth_user

private 
def auth_user
   unless session[:user_id]
   flash[:notice] = '请登录'
   redirect_to new_session_path
   end
end

Routes 详解

规定了特定格式的 URL 请求到后端 Controlleraction 的分发规则

自上而下的路由

例子

# GET /users/2
# method url  controller#action
get '/users/:id', to: 'users#show'
# or
get '/users/:id' => 'users#show'


# 命名路由 => 通过 :as 选项,我们可以为路由命名
# logout GET    /exit(.:format)  sessions#destroy
get 'exit', to: 'sessions#destroy', as: :logout

RESTful 资源路由

# 同时定义多个资源
resources :photos, :books, :videos

# ==> 

resources :photos
resources :books
resources :videos

# 单数资源
# 有时我们希望不使用 ID 就能查找资源。例如,让 /profile 总是显示当前登录用户的个人信息
get 'profile', to: 'users#show'
# ==> 
get 'profile', to: :show, controller: 'users'

命名空间 namespace

namespace :admin do
   resources :articles
end
HTTP 方法 路径 控制器#动作 具名辅助方法
GET /admin/articles admin/articles#index admin_articles_path
GET /admin/articles/new admin/articles#new new_admin_article_path
POST /admin/articles admin/articles#create admin_articles_path
GET /admin/articles/:id admin/articles#show admin_article_path(:id)
GET /admin/articles/:id/edit admin/articles#edit edit_admin_article_path(:id)
PATCH/PUT /admin/articles/:id admin/articles#update admin_article_path(:id)
DELETE /admin/articles/:id admin/articles#destroy admin_article_path(:id)

如果我想要 /articles, 而不是 /admin/articals, 在 admin 文件夹下

# url 改变了, 但是 文件还是在 admin 对应的文件夹下
# 可以设置多个 resources
# module 模块 admin 模块下
scope module: 'admin' do
  resources :articles, :comments
end
# ==> 单个资源科以这样声明
resources :articles, module: 'admin'

但是如果我想要 /admin/articals, 但是不在 admin 文件夹下

# url 是 /admin/articles, 不在 admin 文件夹下
scope '/admin' do
  resources :articles, :comments
end

# 单个资源
resources :articles, path: '/admin/articles'

嵌套路由

class User < ApplicationRecord
  has_many :blogs
end
 
class Blog < ApplicationRecord
  belongs_to :user
end
# 嵌套资源的层级不应超过 1 层。
resources :users do
  resources :blogs
end
resources :users, only: [:index, :destroy]

集合路由(collection route)和成员路由(member route)

resources :users do
   # 成员路由, 一般 带 id 的
   # post /users/:id/status => users#status
   member do
      post :status
   end

   # 集合路由, 不带 id 的
   # get /users/online => users#online
   collection do
      get :online
   end
end

# 如果只需要定义一条方法 => :on 选项
resources :users do
   post :status, on: :member
   get :online, on: :collection
end

非资源式路由
除了资源路由之外,对于把任意 URL 地址映射到控制器动作的路由,Rails 也提供了强大的支持。和资源路由自动生成一系列路由不同,这时我们需要分别声明各个路由

# () 里面的内容为可选参数
# /photos/1  or /photos  photos#display
# 绑定参数
get 'photos(/:id)', to: :display

# 动态片段
# /photos/1/2  params[:id] = 1  params[:user_id] = 2
get 'photos/:id/:user_id', to: 'photos#show'

# 静态片段
# /photos/1/with_user/2
get 'photos/:id/with_user/:user_id', to: 'photos#show'

# 查询字符串
# /photos/1?user_id=2  -> id: 1 user_id: 2
get 'photos/:id', to: 'photos#show'

# 定义默认值
# /photos/12 -> show action; params[:format] = 'jpg'
get 'photos/:id', to: 'photos#show', defaults: { format: 'jpg' }

# http 方法约束
match 'photos', to: 'photos#show', via: [:get, :post]
match 'photos', to: 'photos#show', via: :all

# 片段约束
# /photos/A12345
get 'photos/:id', to: 'photos#show', constraints: { id: /[A-Z]\d{5}/ }
# ->
get 'photos/:id', to: 'photos#show', id: /[A-Z]\d{5}/

# 重定向
get '/stories', to: redirect('/articles')
get '/stories/:name', to: redirect('/articles/%{name}')

自定义资源路由

# :controller 选项用于显式指定资源使用的控制器
resources :photos, controller: 'images'
# -> get /photos iamges#index

# 命名空间中额控制器目录表示法
resources :user_permissions, controller: 'admin/user_permissions'

# 覆盖具名路由辅助方法
# GET   /photos photos#index    as -> images_path
resources :photos, as: 'images'

创建带有命名空间的 controller

bin/rails g controller admin::users

路由创建命名空间 & 添加 search

namespace :admin do
   # admin 下跟路由
   root 'users#index'
   # users 资源路由
   resources :users do
      # 创建 search
      collection do
         get :search
      end
   end
end

View

<%  %> # => 没有输出, 用于循环 遍历 赋值等操作
<%=  %> # => 输出内容

Render 作用

render in controller

# 避免多次 render
def index
   @blogs = Blog.all
   if @blogs
      render 'new'
   end

   render 'edit'
end
def search
   @users = User.all

   # 渲染到 index 动作 页面 
   render action: :index
   # 渲染其他控制器视图
   render "products/show"
end


def search
   # ... render others
   render plain: "OK" # 纯文本
   # html  html_safe 防止转义
   render html: "<strong>Not Found</strong>".html_safe, status: 200
   # 渲染 json
   render json: @product, status: 404
   render json: {
      status: 'ok',
      msg: 'success'
   }
   # 渲染 xml
   render xml: @product, status: :no_content
   # 渲染普通 js
   render js: "alert('Hello Rails');"
   # 渲染文件
   render file: "/path/to/rails/app/views/books/edit.html.erb"
end
# :layout -> 视图使用 
# :location -> 用于设置 HTTP Location 首部
# :content_type
render file: filename, content_type: 'application/pdf'
# :status
render status: 500
render status: :forbidden

redirect_to 重定向

redirect_to user_path, status: 200
# 可以调试 api http 请求
curl -i http://localhostt:3000

form helper

<%= form_for @article, url: {action: "create"}, html: {class: "nifty_form"} do |f| %>
  <%= f.text_field :title %>
  <%= f.text_area :body, size: "60x12" %>
  <%= f.submit "Create" %>
<% end %>

CSRF Cross-site Resouces Forgery

原理: 通过在页面中包含恶意代码或链接,访问已验证用户才能访问的 Web 应用。如果该 Web 应用的会话未超时,攻击者就能执行未经授权的操作

  1. 示例1
    • Bob 在访问留言板时浏览了一篇黑客发布的帖子,其中有一个精心设计的 HTML 图像元素。这个元素实际指向的是 Bob 的项目管理应用中的某个操作,而不是真正的图像文件:<img src="http://www.webapp.com/project/1/destroy">
    • Bob 在 www.webapp.com 上的会话仍然是活动的,因为几分钟前他访问这个应用后没有退出。
    • 当 Bob 浏览这篇帖子时,浏览器发现了这个图像标签,于是尝试从 www.webapp.com 中加载图像。如前文所述,浏览器在发送请求时包含 cookie,其中就有有效的会话 ID
  2. 允许用户自定义 超链接
// 注入 js
<a href="http://www.harmless.com/" onclick="
  var f = document.createElement('form');
  f.style.display = 'none';
  this.parentNode.appendChild(f);
  f.method = 'POST';
  f.action = 'http://www.example.com/account/destroy';
  f.submit();
  return false;">To the harmless survey</a>

<img src="http://www.harmless.com/img" width="400" height="400" onmouseover="..." />
  1. rails 应对措施
    • 使用正确的 http 请求
    • protect_from_forgery with: :exception
    • 对用户输入做限制

Controller


  1. 使用

    • app/controllers 目录
    • 命名规则 names_controller.rb
    • 支持命名空间, 以module组织
  2. Instance Methods in Controller

    • params
      • 获取http请求中 get post的 参数
      • 可以使用Symbol 和 String的方式访问, 比如params[:user] params['user']
    • session & cookies
    • render -> 上面有讲
    • redirect_to
    • send_data & send_file -> 以数据流的方式发送数据
      • send_file 方法很方便,只要提供磁盘中文件的名称,就会用数据流发送文件内容
      def download
         send_file '/file/to/download.pdf'
      end
      def download
         send_data image.data, type: image.content_type
      end
      
    • request
      • request.get? request.headers request.query_string etc...
    • response
      response.location response.body etc...
  3. Class Methods in Controller

    • Filters
      • before_action after_action around_action

Exception 异常

  1. 捕获错误后如果想做更详尽的处理,可以使用 rescue_from, rescue_from 可以处理整个控制器及其子类中的某种(或多种)异常
class ApplicationController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound, with: :record_not_found

  private

    def record_not_found
      render plain: "404 Not Found", status: 404
    end
end
  1. 不过只要你能捕获异常,就可以做任何想做的处理。例如,可以新建一个异常类,当用户无权查看页面时抛出
class ApplicationController < ActionController::Base
  rescue_from User::NotAuthorized, with: :user_not_authorized
  private
    def user_not_authorized
      flash[:error] = "You don't have access to this section."
      redirect_back(fallback_location: root_path)
    end
end

class ClientsController < ApplicationController
  # 检查是否授权用户访问客户信息
  before_action :check_authorization

  # 注意,这个动作无需关心任何身份验证操作
  def edit
    @client = Client.find(params[:id])
  end

  private

    # 如果用户没有授权,抛出异常
    def check_authorization
      raise User::NotAuthorized unless current_user.admin?
    end
end

Model 了解

  1. 约束

    • 表名(table_name)
    # user model 对应数据库中的 users 表
    class user < ActiveRecord::Base
    end
    
    • columns
      • 对应我的 users 表, column -> id username password created_at updated_at
  2. 覆盖表名

    class Product < ApplicationRecord
       self.primary_key = "product_id" # 指定表的主键
       self.table_name = "my_products" # default -> products
    end
    
  3. CURD

    • new 方法创建一个新对象,create 方法创建新对象,并将其存入数据库
    class user < ActiveRecord::Base
    end
    
    # create
    # 1
    user = User.new username = "David", password = "123456"
    user.save
    # 2
    User.create username = "David", password = "123456"
    
    # Reader
    users = User.all # 返回所有用户组成的集合
    user = User.first # 返回第一个用户
    david = User.find_by(name: 'David') # 返回第一个名为 David 的用户
    # 查找所有名为 David,职业为 Code Artists 的用户,而且按照 created_at 反向排列
    users = User.where(name: 'David', occupation: 'Code Artist').order(created_at: :desc)
    
    # Update
    # 1
    user = User.find_by(name: 'David')
    user.name = 'Dave'
    user.save
    # 2
    user = User.find_by(name: 'David')
    user.update(name: 'Dave')
    # 3
    User.update_all "max_login_attempts = 3, must_change_password = 'true'"
    
    # Delete
    user = User.find_by(name: 'David')
    user.destroy
    
  4. 表间关联关系

# 示例
class User < ActiveRecord::Base
   has_many :blogs
end
class Blog < ActiveRecord::Base
   belongs_to  :user
end

model table column key
User  users  id primary_key => User 模型 users 表 id 字段 主键
Blog blogs user_id foreign_key => Blog 模型 blogs表 user_id 字段 外键
  1. 多对多
class Blog < ApplicationRecord
  has_and_belongs_to_many :tags
end

class Tag < ApplicationRecord
  has_and_belongs_to_many :blogs
end

# 创建表
class CreateTags < ActiveRecord::Migration[6.0]
  def change
    create_table :tags do |t|
      t.string :title
      t.timestamps
    end
    create_table :blogs_tags do |t|
      t.integer :blog_id
      t.integer :tag_id
    end
  end
end

  1. CURD 深入
# 查找

# find vs find_by 区别
# 1. 传参不同; 2: 错误时 find_by 返回 nil, find 抛出异常; 3. find 只允许传入 id, find_by 可以传入其他参数
find(id) # 直接传入 id
find_by(id: 1) # 传入 hash 值
find_by! # ! 错误会抛出异常

# 指定 sql 语句 来查询数据库
find_by_sql # 总是返回对象的数组
User.find_by_sql "select * from users where id = 1"

ids # 获得关联的所有 ID
User.ids # [1, 10, 11, 12]


# where
# 在没有操作之前. 只是封装查询对象
Blog.where(["id = ? and title = ?", params[:id], params[:title]]) # 自定义数组 ? 替换
Blog.where(id: 3, title: 'test') # hash 的模式

# 什么是: n+1 查询 => 例如博客列表页面, 我们一次请求 列表次数的 sql, 但每个列表都有 标签. 每行列表都要请求一次 标签 sql, 所以是 n + 1 次 查询
includes(:tags, :user) # 自动把关联关系一次查出来

# 更新

# Blog 模型

# 1
b = Blog.first
b.title = 'chenges'
b.save

# 2 update_attributes  赋值 + save
b.update_attributes title: 'test', content: 'content'

# 3. update_attribute 赋值 + save
b.update_attribute :title, '我是标题'

changed? # 返回 boolean
changed_attributes # 返回上次记录

# ! 方法
# 会抛出异常
save!

create!

update_attributes!
# Exception
ActiveRecord::RecordNotFound # 未发现记录
ActiveRecord::UnknownAttributeError  # 未知属性
ActiveRecord::RecordInvalid # 验证未通过
ActiveRecord::Rollback # 
etc...
  1. Model 自定义属性
class Blog < ApplicationRecord
  def tags_string= one_tags
    one_tags.split(',').each do |tag|
      one_tag = Tag.find_by(title: tag)
      one_tag = Tag.new(title: tag) unless one_tag
      # 博客就在当前范围
      self.tags << one_tag
    end
  end

  def content= one_content
   write_attributes :content, one_content * 2
  end

  # 转化拼音
  def title_pinyib 
   Pingyin.t self.title
  end
end
  1. 事务 transaction
# 会触发 rollback
Blog.transaction do
   blog.save!
end

# 会一直触发 rollback
Blog.transaction do
   raise 'error'
end

数据验证

class Person < ApplicationRecord
  validates :name, presence: true
  valiedates_prensence_of :name
end
class Person < ApplicationRecord
  validates :name, presence: true
end
 
# valid? 方法会触发数据验证,如果对象上没有错误,返回 true,否则返回 false
Person.create(name: "John Doe").valid? # => true
Person.create(name: nil).valid? # => false
# on 啥时候
class Person < ApplicationRecord
  # 更新时允许电子邮件地址重复
  validates :email, uniqueness: true, on: :create
 
  # 创建记录时允许年龄不是数字
  validates :age, numericality: true, on: :update
 
  # 默认行为(创建和更新时都验证)
  validates :name, presence: true
end

scopes 作用域

# 定义作用域和通过定义类方法来定义作用域效果完全相同
class User < ApplicationRecord
   scope :published, -> { where(published: true) }

   # 等同于
   def self.published
     where(published: true)
   end
end

# 调用
User.published.first
User.published.new
# 传入参数
class Article < ApplicationRecord
  # 创建之前
  scope :created_before, ->(time) { where("created_at < ?", time) }
end

Article.created_before(Time.zone.now)

关联关系参数

class User < ActiveRecord::Base
   has_many :blogs

   # -> 代码块, 类的名称: Blog 类似 scope
   has_many :public_blogs, -> { where(is_public: true) }, class_name: :Blog

   # 查出用户最新的一篇博客 has_one 查一个
   has_one :latest_blog, -> { order("id desc") }, class_name: :Blog

   # 自身关联 用户里面有管理者 和 普通员工
   has_many :普通人, class_name: :User, foreign_key: 'manager_id'
   belongs_to :管理者, class_name: :User
end

# 自身关联的使用
@user.普通人 和 @user.管理者

# 约束复习
blogs -> Blog # blogs 对应 Blog Model 
User -> user_id
User primary_key -> id
Blog foreign_key -> user_id
# 指定关联参数
class User < ActiveRecord::Base
   # 模型名称 class_name 指定, 主键: primary_key 指定, 外键: foreign_key
   has_many :blogs, class_name: :Blog, primary_key: :id, foreign_key: :user_id
end

# :dependent
class User < ActiveRecord::Base
   # :dependent 选项控制属主销毁后怎么处理关联的对象
   # :destroy 销毁关联的对象
   has_many :blogs, dependent: :destroy
end

user = User.first
user.destroy # blogs 也会被销毁

Migration

  1. 作用
  1. 使用
bin/rails g model
# 已存在的 model, 添加或者修改一些字段, 不需要生成 model, 只需要生成一个单独的移植文件
bin/rails g migration

bin/rails db:migrate
# 示例: 为用户添加一个 style 字段
# bin/rails g migration add_users_style_column
class AddUsersStyleColumn < ActiveRecord::Migration[6.0]
  def change
    # 添加字段 :针对 users 表, :字段名, :字段类型 
    add_column :users, :style, :string
  end
end
  1. 运行迁移
# 常用 调用所有未运行的迁移中的 change up 方法
bin/rails db:migrate (VERSION=20080906120000) # version 可选参数

# 查看迁移的状态
bin/rails db:migrate:status
# up     20211019121601  Create tags
# down   20211021070702  Add users style column

# 回滚 : 通过撤销 change 方法或调用 down 方法来回滚最后一个迁移
bin/rails db:rollback
# 会撤销最后三个迁移
bin/rails db:rollback STEP=3
class AddUsersStyleColumn < ActiveRecord::Migration[6.0]
   # migrate 运行
  def up
    add_column :users, :style, :string
  end
  # rollback 运行
  def dowm
   remove_column :users, :style, :string
  end
end

class AddUsersStyleColumn < ActiveRecord::Migration[6.0]
  def change
    # 添加字段
    add_column :users, :style, :string
    # 删除字段
    remove_column :table_name, :column_name
    # 修改字段
    change_column :table_name, :column_name, :column_type, :column_options
    # 重命名
    rename_column :table_name, :old_column_name, :new_column_name
  end
end

Callback

什么是 callback -> 回调是在对象生命周期的某些时刻被调用的方法。通过回调,我们可以编写在创建、保存、更新、删除、验证或从数据库中加载 Active Record 对象时执行的代码

  1. 回调触发分类
class User < ApplicationRecord
  before_save do
    self.username = self.username.downcase
  end

  # or
  before_save :update_username

  private
  def update_username
    self.username = self.username.downcase
  end
end
# before_save & after_save
class User < ApplicationRecord
  # 是在保存之前调用, 直接赋值即可
  before_save do
    self.username = self.username.downcase
  end

  # 保存之后, 所以需要 save
  # 做一些和本模型无关的操作, 其他模型相关操作
  after_save do
    self.username = self.username.downcase
    # 会死循环
    self.save # update_column 代替
  end
end
上一篇下一篇

猜你喜欢

热点阅读