<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>全栈 on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/%E5%85%A8%E6%A0%88/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Mon, 14 Sep 2026 10:00:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/%E5%85%A8%E6%A0%88/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>Ruby on Rails 从零基础到精通：环境搭建、MVC、Active Record、Hotwire 与生产部署全指南</title>
      <link>https://ftmao.com/blog/posts/ruby-on-rails-beginner-to-mastery/</link>
      <pubDate>Mon, 14 Sep 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/ruby-on-rails-beginner-to-mastery/</guid>
      <description>&lt;h1 id=&#34;ruby-on-rails-从零基础到精通环境搭建mvcactive-recordhotwire-与生产部署全指南&#34;&gt;Ruby on Rails 从零基础到精通：环境搭建、MVC、Active Record、Hotwire 与生产部署全指南&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;目标读者：零基础想学前端之外的第一门后端框架的人，或者写过 Spring/Django/Laravel、想快速搞清楚 Rails 到底「优雅在哪」的老手。
写作立场：不吹不黑。Rails 不是银弹，但它是「一个人/小团队把想法变成产品」这条赛道上至今没有对手的工具。
姊妹篇：《&lt;a href=&#34;https://ftmao.com/blog/blog/posts/ruby-on-rails-advanced-external-components/&#34;&gt;Ruby on Rails 高级篇：接入 Redis、Elasticsearch、消息队列与大数据组件&lt;/a&gt;》——本篇讲「把功能写出来」，高级篇讲「把系统接出去」。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;全文八篇，顺序就是学习顺序：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;一 环境搭建&amp;lt;br/&amp;gt;30 分钟跑起第一个应用&amp;#34;] --&amp;gt; B[&amp;#34;二 Ruby 速成&amp;lt;br/&amp;gt;只学 Rails 用得上的部分&amp;#34;]
    B --&amp;gt; C[&amp;#34;三 MVC 骨架&amp;lt;br/&amp;gt;约定优于配置&amp;#34;]
    C --&amp;gt; D[&amp;#34;四 Active Record&amp;lt;br/&amp;gt;数据库即对象&amp;#34;]
    D --&amp;gt; E[&amp;#34;五 视图与 Hotwire&amp;lt;br/&amp;gt;不写 JS 的现代前端&amp;#34;]
    E --&amp;gt; F[&amp;#34;六 进阶四件套&amp;lt;br/&amp;gt;任务 缓存 邮件 文件&amp;#34;]
    F --&amp;gt; G[&amp;#34;七 安全 API 与测试&amp;lt;br/&amp;gt;敢交付的标准&amp;#34;]
    G --&amp;gt; H[&amp;#34;八 生产部署&amp;lt;br/&amp;gt;Docker + Kamal 上线&amp;#34;]
    style H fill:#e8f4e8&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;为什么是这个顺序？前两篇解决「&lt;strong&gt;能不能跑起来&lt;/strong&gt;」；三、四、五篇解决「&lt;strong&gt;能不能写出完整功能&lt;/strong&gt;」——这是 Rails 的精华区；第六篇解决「&lt;strong&gt;真实产品的边角需求&lt;/strong&gt;」；第七篇解决「&lt;strong&gt;敢不敢给别人用&lt;/strong&gt;」；第八篇解决「&lt;strong&gt;能不能扛住真实流量&lt;/strong&gt;」。&lt;/p&gt;
&lt;p&gt;在开始前先回答一个 2026 年绕不开的问题：&lt;strong&gt;Rails 还值得学吗？&lt;/strong&gt; 我的判断：如果你的目标是大厂后端岗位，去学 Java/Go；如果你的目标是独立开发、创业 MVP、小团队快速交付，Rails 依然是第一选择——Shopify、GitHub、Basecamp、Hulu 都跑在 Rails 上，AI 编程工具对 Rails 这种「约定极强、模式高度统一」的框架生成的代码质量也明显更好。想清楚再投入。&lt;/p&gt;
&lt;h3 id=&#34;为什么硅谷有人在用-rails与-java-的正面对比&#34;&gt;为什么硅谷有人在用 Rails？与 Java 的正面对比&lt;/h3&gt;
&lt;p&gt;Rails 在硅谷的流行，核心在于它曾是「&lt;strong&gt;创业公司快速验证想法&lt;/strong&gt;」的代名词。对比 Java，它的优势与劣势都源于同一个特点：&lt;strong&gt;极致的开发效率，以牺牲部分运行性能和控制权为代价&lt;/strong&gt;。&lt;/p&gt;
&lt;h4 id=&#34;-硅谷为什么选-rails&#34;&gt;💎 硅谷为什么选 Rails&lt;/h4&gt;
&lt;p&gt;硅谷文化崇尚「快速失败、快速迭代」，Rails 恰好精准命中了早期创业公司的核心诉求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;与创业浪潮同步崛起&lt;/strong&gt;：Rails 和 Y Combinator 几乎同时起步，早期有 90% 的 YC 创业公司用 Rails 构建产品——Airbnb、GitHub、Shopify、Twitch 都是靠 Rails 起家的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;极致的 Time-to-Market&lt;/strong&gt;：硅谷创业最怕浪费时间。「约定优于配置」加上大量开箱即用的功能，让 1-3 人的小团队 &lt;strong&gt;4-6 周就能上线一个 SaaS 产品的 MVP&lt;/strong&gt;，开发速度比 Java 系框架快 30%-40%。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;降低早期成本与风险&lt;/strong&gt;：创业初期资金有限，Rails 是全栈框架，一个开发者搞定前后端，人力和服务器成本都压到最低。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;-对比-java一体两面的哲学&#34;&gt;⚖️ 对比 Java：一体两面的哲学&lt;/h4&gt;
&lt;p&gt;两者代表了完全不同的价值观：&lt;strong&gt;Rails 追求「开发者幸福感」与速度，Java 追求「工业级稳健」与性能&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Rails 的优势（Java 的代价）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;开发效率碾压&lt;/strong&gt;：Ruby 语法接近自然语言，Rails 零样板配置；Java/Spring Boot 配置繁琐、样板代码多，同样功能代码量明显更大。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码更少、更优雅&lt;/strong&gt;：&lt;code&gt;User.where(active: true)&lt;/code&gt; 读起来就是英语；Java 完成同样的事通常要写更多仪式性代码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更适合 MVP 与快速迭代&lt;/strong&gt;：需求不明确、频繁试错的阶段，Rails 让你专注业务逻辑而非基础设施。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Java 的优势（Rails 的短板）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;性能与并发&lt;/strong&gt;：Java 是编译型语言，JVM 在多线程和 CPU 密集型任务上性能远超 Ruby；Rails 面对超高并发往往需要堆更多机器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;静态类型与确定性&lt;/strong&gt;：编译期就能抓住大量错误，对金融、医疗等强监管行业至关重要；Ruby 的动态类型和元编程「魔法」在灵活的同时也埋下运行时风险。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生态与人才池&lt;/strong&gt;：Java 拥有全球最大的开发者社区和企业级库，招聘容易；资深 Rails 开发者相对稀缺，招人成本和难度更高。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一句话总结：&lt;strong&gt;Rails 是创业公司从 0 到 1 的加速器——用最少的人、最快的时间把产品扔到市场上验证；Java 是企业从 1 到 100 的稳定基石——用性能、类型安全和庞大生态承载大规模、高并发的核心业务。&lt;/strong&gt; 这也呼应了开头的判断：先想清楚自己在哪条赛道上，再决定要不要投入。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第一篇-环境搭建一次装对终身受益&#34;&gt;第一篇 环境搭建：一次装对，终身受益&lt;/h2&gt;
&lt;p&gt;Rails 环境搭建的核心矛盾是：&lt;strong&gt;Ruby 版本会过时，系统自带 Ruby 不能用&lt;/strong&gt;。macOS 自带的 Ruby 是给系统脚本用的，别碰；Windows 原生支持差，别硬刚。&lt;/p&gt;
&lt;h3 id=&#34;11-各平台的正确姿势&#34;&gt;1.1 各平台的正确姿势&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    Q{你的操作系统?}
    Q --&amp;gt;|macOS / Linux| A[&amp;#34;mise 或 rbenv&amp;lt;br/&amp;gt;管理 Ruby 版本&amp;#34;]
    Q --&amp;gt;|Windows| B[&amp;#34;WSL2 + Ubuntu&amp;lt;br/&amp;gt;然后按 Linux 流程走&amp;#34;]
    A --&amp;gt; C[&amp;#34;安装 Ruby 3.3+&amp;#34;]
    B --&amp;gt; C
    C --&amp;gt; D[&amp;#34;gem install rails&amp;#34;]
    D --&amp;gt; E[&amp;#34;rails new 验证&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;macOS / Linux（推荐 mise，一个工具管所有语言版本）：&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 安装 mise（Ruby/Node/Python 版本管理一把梭）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;curl https://mise.run | sh
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 安装 Ruby（mise 会自动装编译依赖）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mise install ruby@3.3
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mise use -g ruby@3.3
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 验证&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ruby -v   &lt;span style=&#34;color:#75715e&#34;&gt;# ruby 3.3.x&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;老教程会教你用 rbenv 或 RVM，都可以，但 mise 更快且一个工具同时管 Node（后面 jsbundling 用得上），2026 年新项目直接 mise。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Windows：&lt;/strong&gt; 装 WSL2（&lt;code&gt;wsl --install&lt;/code&gt;），进 Ubuntu 后按上面的 Linux 流程走。不要尝试 RubyInstaller + 原生 Windows 开发，你会在 native gem 编译上浪费人生。&lt;/p&gt;
&lt;h3 id=&#34;12-安装-rails-并创建第一个应用&#34;&gt;1.2 安装 Rails 并创建第一个应用&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Rails 本身是一个 gem&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;gem install rails
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;rails -v   &lt;span style=&#34;color:#75715e&#34;&gt;# Rails 8.x&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 创建应用（默认 SQLite，零配置即可跑）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;rails new blog
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cd blog
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 启动&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;bin/rails server&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;打开 http://localhost:3000 ，看到 Rails 欢迎页，环境关就过了。&lt;/p&gt;
&lt;h3 id=&#34;13-新人必踩的三个坑&#34;&gt;1.3 新人必踩的三个坑&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;gem 安装慢/失败&lt;/strong&gt;：国内网络把 RubyGems 源换成镜像：&lt;code&gt;gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/&lt;/code&gt;，Bundler 同理 &lt;code&gt;bundle config mirror.https://rubygems.org https://gems.ruby-china.com&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;native extension 编译失败&lt;/strong&gt;（&lt;code&gt;pg&lt;/code&gt;、&lt;code&gt;mysql2&lt;/code&gt;、&lt;code&gt;nokogiri&lt;/code&gt; 报错）：缺编译链。macOS 装 &lt;code&gt;xcode-select --install&lt;/code&gt;，Ubuntu 装 &lt;code&gt;sudo apt install build-essential libssl-dev libyaml-dev zlib1g-dev&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据库选型&lt;/strong&gt;：学习期用默认的 SQLite 就好，它比你想象的能打（Rails 8 的 Solid Queue/Cache/Cable 全都支持 SQLite 后端）；确认要长期做的项目，创建时直接 &lt;code&gt;rails new blog -d postgresql&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;14-编辑器&#34;&gt;1.4 编辑器&lt;/h3&gt;
&lt;p&gt;VS Code + Ruby LSP 插件（Shopify 出品，官方维护）是当前最佳组合。RubyMine 功能全但收费且重，个人选择。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第二篇-ruby-速成只学-rails-用得上的-30&#34;&gt;第二篇 Ruby 速成：只学 Rails 用得上的 30%&lt;/h2&gt;
&lt;p&gt;很多人学 Rails 失败的原因是先花三个月啃完整本 Ruby 书。&lt;strong&gt;不要这样做。&lt;/strong&gt; Rails 用到的 Ruby 是有倾向的，先掌握下面这些，剩下的边写边查。&lt;/p&gt;
&lt;h3 id=&#34;21-一切都是对象包括没有值&#34;&gt;2.1 一切都是对象，包括「没有值」&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;times { print &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Hi &amp;#34;&lt;/span&gt; }      &lt;span style=&#34;color:#75715e&#34;&gt;# 数字有方法&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;hello&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;upcase                &lt;span style=&#34;color:#75715e&#34;&gt;# 字符串有方法&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;nil?                      &lt;span style=&#34;color:#75715e&#34;&gt;# =&amp;gt; true，nil 也是对象&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;22-symbolrails-里满天飞的-thing&#34;&gt;2.2 Symbol：Rails 里满天飞的 &lt;code&gt;:thing&lt;/code&gt;&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;:name&lt;/span&gt;   &lt;span style=&#34;color:#75715e&#34;&gt;# Symbol， immutable、全局唯一、省内存&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;name&amp;#34;&lt;/span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;# String&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 你在 Rails 里见到的 90% 的 Symbol 用法本质是「当 key 用」&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;params&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;:page&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;User&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;find_by(&lt;span style=&#34;color:#e6db74&#34;&gt;email&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;a@b.com&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;判断标准：这个值是「数据」用字符串，是「标识/选项/key」用 Symbol。&lt;/p&gt;
&lt;h3 id=&#34;23-blockruby-的灵魂rails-魔法的地基&#34;&gt;2.3 Block：Ruby 的灵魂，Rails 魔法的地基&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# block 是一段可以传给方法的代码&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;].&lt;/span&gt;map { &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt;n&lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; n &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt; }        &lt;span style=&#34;color:#75715e&#34;&gt;# =&amp;gt; [2, 4, 6]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;].&lt;/span&gt;select { &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt;n&lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; n&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;odd? }    &lt;span style=&#34;color:#75715e&#34;&gt;# =&amp;gt; [1, 3]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 你在 Rails 里写的这个：&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;Rails&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;application&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;routes&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;draw &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  resources &lt;span style=&#34;color:#e6db74&#34;&gt;:posts&lt;/span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# ← do...end 就是 block&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;看懂 block，&lt;code&gt;each/map/select/reduce&lt;/code&gt; 四个方法练熟，Ruby 集合操作就毕业了。&lt;strong&gt;戒掉 for 循环&lt;/strong&gt;，Ruby 里没人写 for。&lt;/p&gt;
&lt;h3 id=&#34;24-hash-与方法调用的语法糖&#34;&gt;2.4 Hash 与方法调用的语法糖&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Ruby 允许省略括号和最外层 Hash 的花括号&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;redirect_to post_path(@post), &lt;span style=&#34;color:#e6db74&#34;&gt;notice&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;创建成功&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 等价于&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;redirect_to(post_path(@post), { &lt;span style=&#34;color:#e6db74&#34;&gt;notice&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;创建成功&amp;#34;&lt;/span&gt; })&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;Rails 代码看起来「像英语句子」全靠这条规则。第一次见会困惑，习惯后回不去。&lt;/p&gt;
&lt;h3 id=&#34;25-module-与-mixinrails-复用代码的方式&#34;&gt;2.5 Module 与 mixin：Rails 复用代码的方式&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;module&lt;/span&gt; Taggable
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;tags_count&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    tags&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;count
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Post&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;ApplicationRecord&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;include&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;Taggable&lt;/span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# Post 的实例获得 tags_count 方法&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;Rails 的 Concern（第六篇讲）就是这个的加强版。&lt;/p&gt;
&lt;h3 id=&#34;26-元编程知道它存在就够了&#34;&gt;2.6 元编程：知道它存在就够了&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;method_missing&lt;/code&gt;、&lt;code&gt;define_method&lt;/code&gt;、&lt;code&gt;send&lt;/code&gt; 是 Rails「魔法」的来源（比如 &lt;code&gt;find_by_email&lt;/code&gt; 这种动态方法）。&lt;strong&gt;学习期只要知道「方法不存在时 Ruby 会走 method_missing 兜底」，能解释魔法、看得懂报错就行&lt;/strong&gt;，不要在自己的业务代码里玩元编程——那是框架作者的特权，不是业务开发者的。&lt;/p&gt;
&lt;h3 id=&#34;27-异常捕获包装与别吞&#34;&gt;2.7 异常：捕获、包装与「别吞」&lt;/h3&gt;
&lt;p&gt;Rails 代码到处是隐式的异常流（&lt;code&gt;save!&lt;/code&gt;、&lt;code&gt;find&lt;/code&gt;、事务回滚全靠它），所以异常机制必须学扎实，不能只学个 &lt;code&gt;rescue&lt;/code&gt; 的形状。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;begin&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  order&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;charge!
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;rescue&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;PaymentGateway&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;Timeout&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; e   &lt;span style=&#34;color:#75715e&#34;&gt;# 只接你认识、能处理的具体异常类&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  schedule_retry(order)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;ensure&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  audit_log!(order)                   &lt;span style=&#34;color:#75715e&#34;&gt;# 无论成败都执行：关连接、记审计&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;三条核心纪律：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;rescue 必须落到具体的类&lt;/strong&gt;。裸 &lt;code&gt;rescue&lt;/code&gt; 默认只接 &lt;code&gt;StandardError&lt;/code&gt;，尚可接受；&lt;code&gt;rescue Exception&lt;/code&gt; 是事故——它把 &lt;code&gt;SystemExit&lt;/code&gt;（Ctrl+C、kill）和 &lt;code&gt;NoMemoryError&lt;/code&gt; 一起吞掉，进程都杀不掉。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在边界上包装异常&lt;/strong&gt;：第三方库的 &lt;code&gt;Faraday::TimeoutError&lt;/code&gt; 不该穿透你的业务层，换成自己的业务异常，Ruby 会自动把原始异常挂到 &lt;code&gt;cause&lt;/code&gt; 链上，排查时不丢现场：&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;PaymentError&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;StandardError&lt;/span&gt;; &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;charge&lt;/span&gt;(order)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  gateway&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;charge(order&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;amount)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;rescue&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;Faraday&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;Error&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; e
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;raise&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;PaymentError&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;支付网关调用失败&amp;#34;&lt;/span&gt;   &lt;span style=&#34;color:#75715e&#34;&gt;# e 自动记录在 cause 上&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;上层从此只认 &lt;code&gt;PaymentError&lt;/code&gt;，不用认识十几个底层异常类——Java 用 checked exception 想解决的就是这个问题，Ruby 用 &lt;code&gt;cause&lt;/code&gt; 链解决得干净得多。
3. &lt;strong&gt;「捕获-吞掉」是头号罪恶&lt;/strong&gt;：&lt;code&gt;rescue&lt;/code&gt; 后只 &lt;code&gt;puts&lt;/code&gt; 一行甚至什么都不做，等于拆掉火警报警器。规则是&lt;strong&gt;要么处理它（补偿、降级、重试），要么包装后继续抛，要么至少记日志再上抛&lt;/strong&gt;。生产环境的未处理异常应该流向 Sentry（§8.3 清单），而不是流向你的 rescue。&lt;/p&gt;
&lt;p&gt;Rails 侧的两个标准捕获入口顺带记住：controller 里 &lt;code&gt;rescue_from ActiveRecord::RecordNotFound, with: :render_404&lt;/code&gt;；Job 里 &lt;code&gt;retry_on Faraday::TimeoutError, wait: :polynomially_longer&lt;/code&gt; / &lt;code&gt;discard_on&lt;/code&gt;——&lt;code&gt;rescue&lt;/code&gt; 块内的 &lt;code&gt;retry&lt;/code&gt; 关键字同理存在但慎用（无退避的无限重试会打爆下游），重试纪律进阶篇 §5.2 展开。&lt;/p&gt;
&lt;h3 id=&#34;28-java-里没有的写法unless方法名尾巴的--和-&#34;&gt;2.8 Java 里没有的写法：unless、方法名尾巴的 &lt;code&gt;!&lt;/code&gt; 和 &lt;code&gt;?&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Java 过来的读者会被这几个写法绊住，集中一次说清。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;unless&lt;/code&gt; 就是反向的 &lt;code&gt;if&lt;/code&gt;&lt;/strong&gt;：&lt;code&gt;unless x&lt;/code&gt; 完全等价于 &lt;code&gt;if !x&lt;/code&gt;。它和 &lt;code&gt;if&lt;/code&gt; 都能后置成修饰符形式，这是 Rails 里满屏的 guard clause 写法：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;unless&lt;/span&gt; user                     &lt;span style=&#34;color:#75715e&#34;&gt;# 等价于 if !user，但没人写后者&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;redirect_to login_path &lt;span style=&#34;color:#66d9ef&#34;&gt;unless&lt;/span&gt; current_user
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;raise&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;验证码已过期&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;unless&lt;/span&gt; code&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;unexpired?&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;经验：&lt;code&gt;unless&lt;/code&gt; 只用于单一否定条件；&lt;strong&gt;&lt;code&gt;unless ... else&lt;/code&gt; 是禁品&lt;/strong&gt;——双重否定读三遍才能绕明白。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方法名尾巴的 &lt;code&gt;!&lt;/code&gt; 和 &lt;code&gt;?&lt;/code&gt; 不是语法，是全民遵守的命名约定&lt;/strong&gt;——Ruby 允许方法名以这两个字符结尾，社区给了它们固定含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;?&lt;/code&gt; 结尾：&lt;strong&gt;谓词方法，返回布尔&lt;/strong&gt;。&lt;code&gt;nil?&lt;/code&gt;、&lt;code&gt;valid?&lt;/code&gt;、&lt;code&gt;user.admin?&lt;/code&gt;——Java 的 &lt;code&gt;isValid()&lt;/code&gt; 在这里写作 &lt;code&gt;valid?&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;!&lt;/code&gt; 结尾：&lt;strong&gt;「危险版」方法&lt;/strong&gt;，相对于不带 &lt;code&gt;!&lt;/code&gt; 的温和版存在：
&lt;ul&gt;
&lt;li&gt;Active Record：&lt;code&gt;save&lt;/code&gt; 失败返回 &lt;code&gt;false&lt;/code&gt;，&lt;code&gt;save!&lt;/code&gt; 失败抛异常（§4.6 说事务块内必须用 &lt;code&gt;!&lt;/code&gt; 版，根源就在这里）；&lt;code&gt;find_by&lt;/code&gt; 找不到返回 &lt;code&gt;nil&lt;/code&gt;，&lt;code&gt;find&lt;/code&gt; 找不到抛 &lt;code&gt;RecordNotFound&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;Ruby 标准库：&lt;code&gt;map&lt;/code&gt; 返回新数组，&lt;code&gt;map!&lt;/code&gt; 原地修改旧数组。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以你在项目里看到的 &lt;code&gt;SmsVerificationCode.verify!&lt;/code&gt;，按约定直接读作「&lt;strong&gt;校验，失败抛异常&lt;/strong&gt;」——它通常会配一个返回 true/false 的温和版 &lt;code&gt;verify&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;其他高频惊讶点：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;user&lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;.&lt;/span&gt;profile&lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;.&lt;/span&gt;avatar    &lt;span style=&#34;color:#75715e&#34;&gt;# &amp;amp;. 安全导航：链上任一环是 nil 就整体返回 nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                         &lt;span style=&#34;color:#75715e&#34;&gt;# 不用再写 Java 式 if (x != null &amp;amp;&amp;amp; x.getY() != null)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@current_user &lt;span style=&#34;color:#f92672&#34;&gt;||=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;User&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;find(session&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;:user_id&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt;)   &lt;span style=&#34;color:#75715e&#34;&gt;# ||=：有值直接用，没值才查（惰性记忆化）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;你好，&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;#{&lt;/span&gt;user&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;name&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;      &lt;span style=&#34;color:#75715e&#34;&gt;# 字符串插值，仅双引号生效；单引号里 #{} 是字面量&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;attr_accessor&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;:name&lt;/span&gt;      &lt;span style=&#34;color:#75715e&#34;&gt;# 一行生成 getter/setter，Java 得靠 Lombok 的 @Data&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;变量名的前缀就是作用域声明&lt;/strong&gt;——Java 靠关键字和声明位置，Ruby 把作用域写进名字本身：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;token          &lt;span style=&#34;color:#75715e&#34;&gt;# 局部变量：方法内有效，出方法即死&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@token         &lt;span style=&#34;color:#75715e&#34;&gt;# 实例变量：属于当前对象（≈ Java 的 this.token）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@@token        &lt;span style=&#34;color:#75715e&#34;&gt;# 类变量：全类及子类共享——几乎别用（多线程踩踏 + 继承链行为反直觉）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$token         &lt;span style=&#34;color:#75715e&#34;&gt;# 全局变量：别用&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;TOKEN&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;xxx&amp;#34;&lt;/span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;# 常量：大写开头，约定不改&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;Rails 里 &lt;code&gt;@&lt;/code&gt; 最重要的一条规则：&lt;strong&gt;controller 里的实例变量会自动传给视图&lt;/strong&gt;——&lt;code&gt;@post&lt;/code&gt; 在 &lt;code&gt;show.html.erb&lt;/code&gt; 里直接可用，这是 controller → view 传值的官方通道（§3.1 的代码就靠它工作）。所以 &lt;code&gt;@current_token = ApiToken.active.find_by(token: token)&lt;/code&gt; 读作：查询一次，存成当前对象的实例变量，本次请求内（含视图）随处可用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;||=&lt;/code&gt; 展开读是 &lt;code&gt;a || (a = b)&lt;/code&gt;，靠 &lt;code&gt;||&lt;/code&gt; 短路求值：&lt;code&gt;a&lt;/code&gt; 有值（Ruby 里只有 &lt;code&gt;nil&lt;/code&gt;/&lt;code&gt;false&lt;/code&gt; 为假）直接用、右边不执行；是 &lt;code&gt;nil&lt;/code&gt; 才执行赋值。前提是 Ruby 读未赋值的实例变量返回 &lt;code&gt;nil&lt;/code&gt; 不报错——效果就是一次请求里 &lt;code&gt;current_user&lt;/code&gt; 被调十几次也只查一次库。陷阱：值本身可能是 &lt;code&gt;false&lt;/code&gt; 时会反复重算，记住即可。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;attr_accessor :name&lt;/code&gt; 写在哪个类的类体里，就给哪个类的实例加读写能力。它不是字段声明（Ruby 没有这东西），是一次方法调用，当场生成两个方法：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;attr_accessor&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;:name&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 等价于手写：def name; @name; end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;#            def name=(value); @name = value; end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;所以 &lt;code&gt;user.name = &amp;quot;x&amp;quot;&lt;/code&gt; 其实是调用 &lt;code&gt;name=&lt;/code&gt; 方法的语法糖；变体 &lt;code&gt;attr_reader&lt;/code&gt;（只读）/&lt;code&gt;attr_writer&lt;/code&gt;（只写）。Rails 边界：&lt;strong&gt;Model 的数据库字段不写它&lt;/strong&gt;（读写方法由表结构自动生成，见 §4.1），它用于普通 Ruby 对象或不落库的虚拟属性，比如 &lt;code&gt;has_secure_password&lt;/code&gt; 的 &lt;code&gt;password&lt;/code&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第三篇-mvc-骨架约定优于配置&#34;&gt;第三篇 MVC 骨架：约定优于配置&lt;/h2&gt;
&lt;h3 id=&#34;31-rails-的世界观&#34;&gt;3.1 Rails 的世界观&lt;/h3&gt;
&lt;p&gt;其他框架让你「配置」应用怎么组织，Rails 直接「规定」好，你只填业务。一个请求的生命周期：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    Req[&amp;#34;浏览器请求&amp;lt;br/&amp;gt;GET /posts/1&amp;#34;] --&amp;gt; R[&amp;#34;Router&amp;lt;br/&amp;gt;config/routes.rb&amp;#34;]
    R --&amp;gt; C[&amp;#34;Controller&amp;lt;br/&amp;gt;PostsController#show&amp;#34;]
    C --&amp;gt; M[&amp;#34;Model&amp;lt;br/&amp;gt;Post.find(1)&amp;#34;]
    M --&amp;gt; DB[(&amp;#34;数据库&amp;#34;)]
    C --&amp;gt; V[&amp;#34;View&amp;lt;br/&amp;gt;show.html.erb&amp;#34;]
    V --&amp;gt; Resp[&amp;#34;HTML 响应&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;对应到代码：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# config/routes.rb —— URL 到控制器的映射表&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;Rails&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;application&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;routes&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;draw &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  resources &lt;span style=&#34;color:#e6db74&#34;&gt;:posts&lt;/span&gt;   &lt;span style=&#34;color:#75715e&#34;&gt;# 一行生成 7 个 RESTful 路由&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# app/controllers/posts_controller.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;PostsController&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;ApplicationController&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;show&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    @post &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;Post&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;find(params&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;:id&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt;)   &lt;span style=&#34;color:#75715e&#34;&gt;# 取数据，放进实例变量&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;                                &lt;span style=&#34;color:#75715e&#34;&gt;# 不写字，默认渲染 show.html.erb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-erb&#34; data-lang=&#34;erb&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;%#&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; app/views/posts/show.html.erb &lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;%&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;h1&lt;/span&gt;&amp;gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;%=&lt;/span&gt; @post&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;title &lt;span style=&#34;color:#75715e&#34;&gt;%&amp;gt;&lt;/span&gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;h1&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;p&lt;/span&gt;&amp;gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;%=&lt;/span&gt; @post&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;body &lt;span style=&#34;color:#75715e&#34;&gt;%&amp;gt;&lt;/span&gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;p&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;code&gt;resources :posts&lt;/code&gt; 一行展开成的 7 个路由，是 RESTful 的标准答案，背下来：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;HTTP 动词&lt;/th&gt;
					&lt;th&gt;路径&lt;/th&gt;
					&lt;th&gt;Action&lt;/th&gt;
					&lt;th&gt;用途&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;GET&lt;/td&gt;
					&lt;td&gt;/posts&lt;/td&gt;
					&lt;td&gt;index&lt;/td&gt;
					&lt;td&gt;列表&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;GET&lt;/td&gt;
					&lt;td&gt;/posts/new&lt;/td&gt;
					&lt;td&gt;new&lt;/td&gt;
					&lt;td&gt;新建表单&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;POST&lt;/td&gt;
					&lt;td&gt;/posts&lt;/td&gt;
					&lt;td&gt;create&lt;/td&gt;
					&lt;td&gt;创建&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;GET&lt;/td&gt;
					&lt;td&gt;/posts/1&lt;/td&gt;
					&lt;td&gt;show&lt;/td&gt;
					&lt;td&gt;详情&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;GET&lt;/td&gt;
					&lt;td&gt;/posts/1/edit&lt;/td&gt;
					&lt;td&gt;edit&lt;/td&gt;
					&lt;td&gt;编辑表单&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;PATCH/PUT&lt;/td&gt;
					&lt;td&gt;/posts/1&lt;/td&gt;
					&lt;td&gt;update&lt;/td&gt;
					&lt;td&gt;更新&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;DELETE&lt;/td&gt;
					&lt;td&gt;/posts/1&lt;/td&gt;
					&lt;td&gt;destroy&lt;/td&gt;
					&lt;td&gt;删除&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;上面代码里的基类名有规律，认一次终身受用。Rails 框架由一组组件 gem 组成，类名分两族：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;类名&lt;/th&gt;
					&lt;th&gt;组件&lt;/th&gt;
					&lt;th&gt;干什么&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;ActiveRecord::Base&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;Active Record&lt;/td&gt;
					&lt;td&gt;ORM，所有 Model 的基类&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;ActionController::Base&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;Action Pack&lt;/td&gt;
					&lt;td&gt;控制器基类（同包还有 Action View 视图层）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;ActiveJob::Base&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;Active Job&lt;/td&gt;
					&lt;td&gt;后台任务基类（§6.1）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;ActionMailer::Base&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;Action Mailer&lt;/td&gt;
					&lt;td&gt;邮件基类（§6.3）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;ActiveSupport&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;Active Support&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;不是基类&lt;/strong&gt;——工具库（Concern、&lt;code&gt;1.week.ago&lt;/code&gt;、&lt;code&gt;instrument&lt;/code&gt; 都出自它），被 require 而不是被继承&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;而你在代码里继承的 &lt;code&gt;ApplicationRecord&lt;/code&gt;、&lt;code&gt;ApplicationController&lt;/code&gt;、&lt;code&gt;ApplicationJob&lt;/code&gt;、&lt;code&gt;ApplicationMailer&lt;/code&gt;，是&lt;strong&gt;每个 Rails 应用创建时自带的一层中间基类&lt;/strong&gt;（在 &lt;code&gt;app/&lt;/code&gt; 目录下，只有几行）：继承链是 &lt;code&gt;Post &amp;lt; ApplicationRecord &amp;lt; ActiveRecord::Base&lt;/code&gt;。这一层的意义是给「全应用通用」的逻辑一个挂载点（比如登录校验放 &lt;code&gt;ApplicationController&lt;/code&gt;）——改框架基类是 monkey patch，改自己的中间层是正常设计。Java 里对应的是公司框架包里自封装的 BaseController/BaseService 中间层，Rails 只是把它做成了官方约定。&lt;/p&gt;
&lt;p&gt;记忆法：&lt;strong&gt;&lt;code&gt;Active*&lt;/code&gt;/&lt;code&gt;Action*&lt;/code&gt; 是框架给的，&lt;code&gt;Application*&lt;/code&gt; 是你自己的&lt;/strong&gt;。Action Cable（WebSocket）、Active Storage（文件上传）、Action Text（富文本）同族命名，后面章节遇到对号入座即可。&lt;/p&gt;
&lt;h3 id=&#34;32-scaffold十分钟看懂全栈&#34;&gt;3.2 Scaffold：十分钟看懂全栈&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;bin/rails generate scaffold Post title:string body:text
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;bin/rails db:migrate
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;bin/rails server&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;访问 http://localhost:3000/posts ，一个带增删改查、表单校验、分页提示的应用已经在跑了。&lt;strong&gt;scaffold 的价值不是给你用，是给你拆&lt;/strong&gt;——把生成的每个文件读一遍，你就见过 Rails 全栈的标准写法了。真实项目里老手写 scaffold 的不多，但它是最好的教材。&lt;/p&gt;
&lt;h3 id=&#34;33-你每天都会用的命令&#34;&gt;3.3 你每天都会用的命令&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;bin/rails console          &lt;span style=&#34;color:#75715e&#34;&gt;# 交互式环境，直接操作 Model 查数据库，调试神器&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;bin/rails routes           &lt;span style=&#34;color:#75715e&#34;&gt;# 列出所有路由&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;bin/rails generate model Post title:string   &lt;span style=&#34;color:#75715e&#34;&gt;# 生成模型+迁移&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;bin/rails db:migrate       &lt;span style=&#34;color:#75715e&#34;&gt;# 执行迁移&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;bin/rails db:seed          &lt;span style=&#34;color:#75715e&#34;&gt;# 灌种子数据&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;bin/rails runner &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;puts User.count&amp;#39;&lt;/span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;# 跑一次性脚本&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;code&gt;rails console&lt;/code&gt; 是 Rails 开发体验的精髓之一：任何时候想验证一个查询、一段逻辑，进 console 直接跑，不用写测试脚本不用重启服务。&lt;/p&gt;
&lt;h3 id=&#34;34-命名约定背下来少踩-80-的坑&#34;&gt;3.4 命名约定（背下来，少踩 80% 的坑）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Model 类名单数首字母大写 &lt;code&gt;Post&lt;/code&gt; → 表名复数小写 &lt;code&gt;posts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;控制器复数 &lt;code&gt;PostsController&lt;/code&gt; → 视图目录 &lt;code&gt;app/views/posts/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;外键 &lt;code&gt;user_id&lt;/code&gt; → 关联 &lt;code&gt;belongs_to :user&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;文件用蛇形 &lt;code&gt;posts_controller.rb&lt;/code&gt;，类用驼峰 &lt;code&gt;PostsController&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;违反约定的代价不是报错，是你要写配置去解释&lt;/strong&gt;——这就是「约定优于配置」的双向含义：守规矩零配置，不守规矩自己扛。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第四篇-active-recordrails-皇冠上的明珠&#34;&gt;第四篇 Active Record：Rails 皇冠上的明珠&lt;/h2&gt;
&lt;h3 id=&#34;41-migration数据库结构的版本控制&#34;&gt;4.1 Migration：数据库结构的版本控制&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;CreatePosts&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;ActiveRecord&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;Migration&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;8&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;change&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    create_table &lt;span style=&#34;color:#e6db74&#34;&gt;:posts&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt;t&lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      t&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;string &lt;span style=&#34;color:#e6db74&#34;&gt;:title&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;null&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      t&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;string &lt;span style=&#34;color:#e6db74&#34;&gt;:slug&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      t&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;text &lt;span style=&#34;color:#e6db74&#34;&gt;:body&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      t&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;references &lt;span style=&#34;color:#e6db74&#34;&gt;:user&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;null&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;foreign_key&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      t&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;timestamps    &lt;span style=&#34;color:#75715e&#34;&gt;# created_at / updated_at 自动生成&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    add_index &lt;span style=&#34;color:#e6db74&#34;&gt;:posts&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;:title&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    add_index &lt;span style=&#34;color:#e6db74&#34;&gt;:posts&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;:slug&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;unique&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;要点：&lt;strong&gt;数据库结构永远通过 migration 改，永远不手改数据库&lt;/strong&gt;。&lt;code&gt;db/schema.rb&lt;/code&gt; 是结果不是源头；&lt;code&gt;bin/rails db:rollback&lt;/code&gt; 回滚；团队协作时 migration 文件冲突要手工理，别直接删别人的。&lt;/p&gt;
&lt;p&gt;这里要点破 Active Record 最反直觉的一条约定：&lt;strong&gt;Model 类里看不到任何字段声明&lt;/strong&gt;。Java 的实体类要写 &lt;code&gt;@Column private String title;&lt;/code&gt;，Rails 的 Model 类一个字都不用写——Active Record 在运行时读取数据库表结构，自动为每个字段生成读写方法。所以下一节的 &lt;code&gt;Post&lt;/code&gt; 类里虽然只有关联声明，&lt;code&gt;post.title&lt;/code&gt;、&lt;code&gt;post.body&lt;/code&gt; 却都是合法调用。字段的唯一真相来源是 migration 和 &lt;code&gt;db/schema.rb&lt;/code&gt;；这也是 Rails 社区流行用 &lt;code&gt;annotate&lt;/code&gt; gem 把字段以注释形式写回 Model 文件头部的原因——代码审查时不用翻 schema。&lt;/p&gt;
&lt;h3 id=&#34;42-关联三行代码建出对象图&#34;&gt;4.2 关联：三行代码建出对象图&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;User&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;ApplicationRecord&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  has_many &lt;span style=&#34;color:#e6db74&#34;&gt;:posts&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;dependent&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;:destroy&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  has_many &lt;span style=&#34;color:#e6db74&#34;&gt;:comments&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;through&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;:posts&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Post&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;ApplicationRecord&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  belongs_to &lt;span style=&#34;color:#e6db74&#34;&gt;:user&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  has_many &lt;span style=&#34;color:#e6db74&#34;&gt;:comments&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;dependent&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;:destroy&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  has_rich_text &lt;span style=&#34;color:#e6db74&#34;&gt;:content&lt;/span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# ActionText 富文本，白送的&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Comment&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;ApplicationRecord&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  belongs_to &lt;span style=&#34;color:#e6db74&#34;&gt;:post&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;然后你可以这样写：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;user&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;posts&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;create(&lt;span style=&#34;color:#e6db74&#34;&gt;title&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Hello&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;post&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;comments&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;where(&lt;span style=&#34;color:#e6db74&#34;&gt;approved&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;order(&lt;span style=&#34;color:#e6db74&#34;&gt;created_at&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;:desc&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;limit(&lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;counter_cache&lt;/code&gt;&lt;/strong&gt;：列表页要显示「每篇文章的评论数」时，别让它每行发一次 &lt;code&gt;COUNT(*)&lt;/code&gt;（N+1 的变种，&lt;code&gt;includes&lt;/code&gt; 救不了——预加载的是完整记录不是计数）。Rails 的内置答案是在父表存计数字段，增删评论时自动维护：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# migration：add_column :posts, :comments_count, :integer, default: 0, null: false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Comment&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;ApplicationRecord&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  belongs_to &lt;span style=&#34;color:#e6db74&#34;&gt;:post&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;counter_cache&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;post&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;comments&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;size   &lt;span style=&#34;color:#75715e&#34;&gt;# 直接读 posts.comments_count 字段，零查询&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;存量数据回填一次：&lt;code&gt;Post.find_each { |p| Post.reset_counters(p.id, :comments) }&lt;/code&gt;。注意它只在 &lt;code&gt;create&lt;/code&gt;/&lt;code&gt;destroy&lt;/code&gt; 时维护，裸 SQL 删除、&lt;code&gt;update_all&lt;/code&gt; 这类跳过回调的操作会让计数漂移——重要业务定期 &lt;code&gt;reset_counters&lt;/code&gt; 校准。&lt;/p&gt;
&lt;p&gt;顺带说清 Rails 的&lt;strong&gt;删除哲学：默认硬删除&lt;/strong&gt;。&lt;code&gt;destroy&lt;/code&gt; 就是真的 &lt;code&gt;DELETE&lt;/code&gt;（&lt;code&gt;delete&lt;/code&gt; 系列跳过回调直接删），框架核心至今没有内置软删除——这是有意的：软删除（&lt;code&gt;deleted_at&lt;/code&gt; + 默认过滤）的隐性成本极高，每个查询、关联、唯一索引都要背一个「排除已删」的条件（&lt;code&gt;paranoia&lt;/code&gt; 系用 &lt;code&gt;default_scope&lt;/code&gt; 全局污染，删过的手机号还能不能注册都成问题），表还永远瘦不下来。社区的现代答案：&lt;strong&gt;「删了还可能要」是审计需求，不是删除需求&lt;/strong&gt;——误删恢复靠备份（§8.3 备份条），谁改了什么靠 &lt;code&gt;paper_trail&lt;/code&gt; 版本快照，合规留存靠归档表/数据管道（进阶篇第四篇）。非要做软删除，用 &lt;code&gt;discard&lt;/code&gt;（显式 scope，不污染全局查询），别用 &lt;code&gt;paranoia&lt;/code&gt; 系。&lt;/p&gt;
&lt;h3 id=&#34;43-校验与回调&#34;&gt;4.3 校验与回调&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Post&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;ApplicationRecord&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  validates &lt;span style=&#34;color:#e6db74&#34;&gt;:title&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;presence&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;length&lt;/span&gt;: { &lt;span style=&#34;color:#e6db74&#34;&gt;maximum&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  validates &lt;span style=&#34;color:#e6db74&#34;&gt;:slug&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;uniqueness&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  before_validation &lt;span style=&#34;color:#e6db74&#34;&gt;:generate_slug&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt;: &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; { slug&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;blank? }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;# 自定义的发布动作：! 结尾是 Ruby 约定——失败抛异常而不是返回 false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;publish!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    update!(&lt;span style=&#34;color:#e6db74&#34;&gt;published_at&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;Time&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;current)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;generate_slug&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;slug &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; title&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;parameterize
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;校验失败时 &lt;code&gt;post.save&lt;/code&gt; 返回 false、&lt;code&gt;post.errors&lt;/code&gt; 给出原因，表单自动回显错误——表单校验在 Rails 里是 Model 层的事，视图几乎白拿。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;回调是双刃剑&lt;/strong&gt;：&lt;code&gt;before_save&lt;/code&gt;、&lt;code&gt;after_create&lt;/code&gt; 写多了会变成「看不见的流程」，半年后没人知道保存一个 Post 到底触发了什么。经验法则：回调只做和对象自身状态有关的事（比如生成 slug），发邮件、推通知这类副作用放第六篇讲的 Job 里。&lt;/p&gt;
&lt;h3 id=&#34;44-查询接口与-n1-问题&#34;&gt;4.4 查询接口与 N+1 问题&lt;/h3&gt;
&lt;p&gt;链式查询读起来像英语：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;Post&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;published
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;where(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;created_at &amp;gt; ?&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;week&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ago)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;order(&lt;span style=&#34;color:#e6db74&#34;&gt;published_at&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;:desc&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;page(params&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;:page&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;然后是所有 ORM 用户的第一课——&lt;strong&gt;N+1&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-erb&#34; data-lang=&#34;erb&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;%#&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; 视图里这样写 &lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;%&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;%&lt;/span&gt; @posts&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;each &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt;post&lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;%&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;%=&lt;/span&gt; post&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;user&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;name &lt;span style=&#34;color:#75715e&#34;&gt;%&amp;gt;&lt;/span&gt;   &lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;%#&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; 每个 post 单独查一次 user！&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;%&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;%&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;%&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;100 篇文章产生 101 次查询。解法一行：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@posts &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;Post&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;includes(&lt;span style=&#34;color:#e6db74&#34;&gt;:user&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;all   &lt;span style=&#34;color:#75715e&#34;&gt;# 预加载，2 次查询搞定&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;生产项目建议装 &lt;code&gt;bullet&lt;/code&gt; gem，开发环境检测到 N+1 直接弹警告，把问题消灭在摇篮里。&lt;/p&gt;
&lt;h3 id=&#34;45-scope-与查询对象&#34;&gt;4.5 Scope 与查询对象&lt;/h3&gt;
&lt;p&gt;scope 做的事一句话：&lt;strong&gt;给一段查询条件起个名字，让它能复用、能链式拼接&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Post&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;ApplicationRecord&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  scope &lt;span style=&#34;color:#e6db74&#34;&gt;:published&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; { where&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;not(&lt;span style=&#34;color:#e6db74&#34;&gt;published_at&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;) }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  scope &lt;span style=&#34;color:#e6db74&#34;&gt;:recent&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; { order(&lt;span style=&#34;color:#e6db74&#34;&gt;published_at&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;:desc&lt;/span&gt;) }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;Post&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;published&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;recent   &lt;span style=&#34;color:#75715e&#34;&gt;# 合成一条 SQL：WHERE published_at IS NOT NULL ORDER BY published_at DESC&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;两个关键点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;scope 返回的是 &lt;code&gt;ActiveRecord::Relation&lt;/code&gt;（未执行的查询对象），不是结果集&lt;/strong&gt;——所以才能继续 &lt;code&gt;.recent.limit(10)&lt;/code&gt; 往下拼，直到真正遍历时才发出一条 SQL。惰性执行是链式组合的前提。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scope :published, -&amp;gt; {...}&lt;/code&gt; 本质等价于类方法 &lt;code&gt;def self.published; where.not(...); end&lt;/code&gt;，scope 只是它的惯用一行形式。惯例：简单条件用 scope，带参数或逻辑复杂就直接写类方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Java 没有完全对应物：Spring Data JPA 的派生查询方法（&lt;code&gt;findByPublishedAtNotNull()&lt;/code&gt;）是「一种条件一个方法名」，不可组合；QueryDSL/Criteria 的 Predicate 可组合但啰嗦；MyBatis 靠动态 SQL 拼。scope 的独特点是&lt;strong&gt;每个片段返回的还是可继续拼的 Relation&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;查询复杂到三屏放不下时，抽 Query Object（&lt;code&gt;app/queries/post_search_query.rb&lt;/code&gt;），别把 SQL 揉在 controller 里。&lt;/p&gt;
&lt;h3 id=&#34;46-事务与并发控制多步写入的正确性底线&#34;&gt;4.6 事务与并发控制：多步写入的正确性底线&lt;/h3&gt;
&lt;p&gt;一次操作要改两张表以上，就必须回答「改到一半挂了怎么办」。答案是事务：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;Account&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;transaction &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  from&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;update!(&lt;span style=&#34;color:#e6db74&#34;&gt;balance&lt;/span&gt;: from&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;balance &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; amount)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  to&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;update!(&lt;span style=&#34;color:#e6db74&#34;&gt;balance&lt;/span&gt;:   to&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;balance &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; amount)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;回滚的唯一扳机是异常&lt;/strong&gt;。块内任何异常（包括 &lt;code&gt;update!&lt;/code&gt; 校验失败抛出的 &lt;code&gt;ActiveRecord::RecordInvalid&lt;/code&gt;）都会让整段 SQL 回滚、异常原样上抛；而 &lt;code&gt;update&lt;/code&gt;（不带 &lt;code&gt;!&lt;/code&gt;）失败只返回 false，&lt;strong&gt;不会触发回滚&lt;/strong&gt;——这是事务代码最高发的坑：块内永远用 &lt;code&gt;!&lt;/code&gt; 结尾的写方法。想回滚但不想抛异常，手动 &lt;code&gt;raise ActiveRecord::Rollback&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;跨表从来不是问题：事务是「连接级」的，和表无关&lt;/strong&gt;。&lt;code&gt;X.transaction&lt;/code&gt; 里的 &lt;code&gt;X&lt;/code&gt; 用哪个类不重要（惯例写 &lt;code&gt;ApplicationRecord&lt;/code&gt;），重要的是块内所有模型连的是同一个库——一个事务里混写 orders、inventory、audit_logs 三张表是天经地义的用法。真正的边界是&lt;strong&gt;不同数据库&lt;/strong&gt;：每个连接各有一个事务，Rails 不支持分布式事务（没有 2PC），跨库一致性走 Saga/Outbox 模式（进阶篇 §3.3）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;事务传播&lt;/strong&gt;：Spring 的 &lt;code&gt;@Transactional(propagation = ...)&lt;/code&gt; 枚举在 Rails 里没有对应物，嵌套 &lt;code&gt;transaction&lt;/code&gt; 只有三种行为：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;ApplicationRecord&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;transaction &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;                        &lt;span style=&#34;color:#75715e&#34;&gt;# 外层：真事务&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;ApplicationRecord&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;transaction &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;                      &lt;span style=&#34;color:#75715e&#34;&gt;# 默认：静默并入外层，内层是空转&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;ApplicationRecord&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;transaction(&lt;span style=&#34;color:#e6db74&#34;&gt;requires_new&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;# savepoint：内层可独立回滚（≈ Spring NESTED）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;注意 &lt;strong&gt;&lt;code&gt;requires_new&lt;/code&gt; ≈ Spring 的 NESTED，不是 REQUIRES_NEW&lt;/strong&gt;：savepoint 仍在同一个事务里，外层回滚会连内层一起带走。Spring 那种「挂起当前事务、另开一个真独立事务」同一连接上物理不可能，Rails 里要换连接（另一个线程/独立连接池）才行——「主流程失败也要留住审计日志」这类需求，正确解法是先落库、再由 Job/MQ 异步消费（进阶篇 §3.3 的 Outbox 就是这个思路），而不是在请求里开第二个事务。&lt;/p&gt;
&lt;p&gt;并发「先读后改」的场景，两个经典武器：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 悲观锁：SELECT ... FOR UPDATE，行被锁住，并发请求排队&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;account&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;with_lock &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  account&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;update!(&lt;span style=&#34;color:#e6db74&#34;&gt;balance&lt;/span&gt;: account&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;balance &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; amount)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 乐观锁：migration 里加 t.integer :lock_version, default: 0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 之后每次更新自动校验版本号，期间被别人改过就抛 ActiveRecord::StaleObjectError&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;经验法则：&lt;strong&gt;能交给数据库约束（唯一索引、外键）的正确性别用代码保证；「先读后改」交给锁；事务负责把它们圈在一起&lt;/strong&gt;。隔离级别不用背——MySQL/PostgreSQL 的默认值对九成 CRUD 够用，真到需要调它的那天，你会知道的。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第五篇-视图与-hotwire不写-javascript-的现代前端&#34;&gt;第五篇 视图与 Hotwire：不写 JavaScript 的现代前端&lt;/h2&gt;
&lt;p&gt;Rails 前端方案在 2026 年的标准答案是 &lt;strong&gt;Hotwire（Turbo + Stimulus）&lt;/strong&gt;，核心思想：HTML 在服务端渲染，通过 WebSocket/HTTP 把「HTML 片段」推到页面局部替换，而不是把数据序列化成 JSON 让前端框架重画。&lt;/p&gt;
&lt;h3 id=&#34;51-erb-与-partial先打好地基&#34;&gt;5.1 ERB 与 Partial：先打好地基&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-erb&#34; data-lang=&#34;erb&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;%#&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; app/views/posts/index.html.erb &lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;%&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;h1&lt;/span&gt;&amp;gt;文章列表&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;h1&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;%=&lt;/span&gt; render @posts &lt;span style=&#34;color:#75715e&#34;&gt;%&amp;gt;&lt;/span&gt;   &lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;%#&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; 自动逐条渲染 _post.html.erb &lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;%&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;%#&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; app/views/posts/_post.html.erb &lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;%&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;article&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;%=&lt;/span&gt; dom_id(post) &lt;span style=&#34;color:#75715e&#34;&gt;%&amp;gt;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;h2&lt;/span&gt;&amp;gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;%=&lt;/span&gt; link_to post&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;title, post &lt;span style=&#34;color:#75715e&#34;&gt;%&amp;gt;&lt;/span&gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;h2&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;time&lt;/span&gt;&amp;gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;%=&lt;/span&gt; l post&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;published_at, format: &lt;span style=&#34;color:#e6db74&#34;&gt;:long&lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;%&amp;gt;&lt;/span&gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;time&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;article&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;表单用 &lt;code&gt;form_with&lt;/code&gt;，校验错误、CSRF token、PATCH 方法伪装全部自动处理：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-erb&#34; data-lang=&#34;erb&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;%=&lt;/span&gt; form_with &lt;span style=&#34;color:#e6db74&#34;&gt;model&lt;/span&gt;: @post &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt;f&lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;%&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;%=&lt;/span&gt; f&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;text_field &lt;span style=&#34;color:#e6db74&#34;&gt;:title&lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;%&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;%=&lt;/span&gt; f&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;text_area &lt;span style=&#34;color:#e6db74&#34;&gt;:body&lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;%&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;%=&lt;/span&gt; f&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;submit &lt;span style=&#34;color:#75715e&#34;&gt;%&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;%&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;%&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;52-turbo三个概念替代一个前端框架&#34;&gt;5.2 Turbo：三个概念替代一个前端框架&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[Turbo Drive&amp;lt;br/&amp;gt;拦截链接/表单&amp;lt;br/&amp;gt;整页无刷新跳转] --&amp;gt; B[Turbo Frames&amp;lt;br/&amp;gt;页面局部区域独立加载/刷新]
    B --&amp;gt; C[Turbo Streams&amp;lt;br/&amp;gt;服务端推送 HTML 片段&amp;lt;br/&amp;gt;增删改页面任意位置]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Drive&lt;/strong&gt;：零代码，默认开启，整站秒变 SPA 体验。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Frames&lt;/strong&gt;：给区块包一层 &lt;code&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt;，框内点击只刷新框。分页、弹窗、行内编辑全靠它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Streams&lt;/strong&gt;：一行 &lt;code&gt;broadcast_replace_to&lt;/code&gt; 实现「甲发了评论，乙的页面实时出现」——不写一行 JS 的实时功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 评论创建后，实时推到所有打开该文章页面的浏览器&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Comment&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;ApplicationRecord&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  after_create_commit &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    broadcast_append_to &lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;post, &lt;span style=&#34;color:#e6db74&#34;&gt;:comments&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                        &lt;span style=&#34;color:#e6db74&#34;&gt;target&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;comments&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                        &lt;span style=&#34;color:#e6db74&#34;&gt;partial&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;comments/comment&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;53-stimulusjs-只剩撒胡椒面&#34;&gt;5.3 Stimulus：JS 只剩「撒胡椒面」&lt;/h3&gt;
&lt;p&gt;必须的交互（复制按钮、下拉菜单、字符计数）用 Stimulus 控制器，一个 controller 管一个行为：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// app/javascript/controllers/clipboard_controller.js
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;Controller&lt;/span&gt; } &lt;span style=&#34;color:#a6e22e&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;@hotwired/stimulus&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;default&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;extends&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Controller&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;targets&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;source&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;copy&lt;/span&gt;() { &lt;span style=&#34;color:#a6e22e&#34;&gt;navigator&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;clipboard&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;writeText&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;sourceTarget&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;value&lt;/span&gt;) }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-erb&#34; data-lang=&#34;erb&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;div&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;data-controller&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;clipboard&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;input&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;data-clipboard-target&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;source&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;value&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;邀请码 ABC123&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;readonly&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;button&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;data-action&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;clipboard#copy&amp;#34;&lt;/span&gt;&amp;gt;复制&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;button&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;什么时候 Hotwire 不够用？&lt;/strong&gt; 富交互的重型前端（在线表格、设计工具、复杂拖拽白板）老老实实上 React/Vue，Rails 退为纯 API。判断标准：页面状态主要是「服务器数据的展示」→ Hotwire；页面状态主要是「客户端本地状态」→ SPA。&lt;/p&gt;
&lt;h3 id=&#34;54-资产管道importmap-还是-bundling&#34;&gt;5.4 资产管道：importmap 还是 bundling？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;默认 &lt;code&gt;importmap-rails&lt;/code&gt;：无 Node、无构建步骤，直接引用 ESM 包。简单项目够用，部署简单一个量级。&lt;/li&gt;
&lt;li&gt;需要 npm 生态深水区（Tailwind 复杂插件、特定库）：&lt;code&gt;rails new blog -j esbuild -c tailwind&lt;/code&gt;，引入 jsbundling + cssbundling。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第六篇-进阶四件套真实产品的边角需求&#34;&gt;第六篇 进阶四件套：真实产品的边角需求&lt;/h2&gt;
&lt;p&gt;博客型 Demo 和产品之间的差距，主要在这四样。&lt;/p&gt;
&lt;h3 id=&#34;61-后台任务慢的事别挡着请求&#34;&gt;6.1 后台任务：慢的事别挡着请求&lt;/h3&gt;
&lt;p&gt;发邮件、生成报表、调第三方 API——凡是超过 200ms 或可能失败的副作用，丢进后台：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ReportJob&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;ApplicationJob&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  queue_as &lt;span style=&#34;color:#e6db74&#34;&gt;:default&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  retry_on &lt;span style=&#34;color:#66d9ef&#34;&gt;Net&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;OpenTimeout&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;wait&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;:exponentially_longer&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;attempts&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;perform&lt;/span&gt;(user)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;UserMailer&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;weekly_report(user)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;deliver_now
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;ReportJob&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;perform_later(current_user)   &lt;span style=&#34;color:#75715e&#34;&gt;# 入队即返回&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;队列后端选型（2026 年现状）：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;方案&lt;/th&gt;
					&lt;th&gt;依赖&lt;/th&gt;
					&lt;th&gt;适用&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Solid Queue&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;只用数据库（Rails 8 默认）&lt;/td&gt;
					&lt;td&gt;中小规模，不想多维护 Redis 的&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Sidekiq&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Redis&lt;/td&gt;
					&lt;td&gt;高吞吐、生态最成熟&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;GoodJob&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;PostgreSQL&lt;/td&gt;
					&lt;td&gt;已用 PG 且想要 cron 定时任务&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;62-缓存俄罗斯套娃&#34;&gt;6.2 缓存：俄罗斯套娃&lt;/h3&gt;
&lt;p&gt;Rails 的缓存哲学是「HTML 片段缓存 + 模型时间戳自动失效」：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-erb&#34; data-lang=&#34;erb&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;%#&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; 每个 post 的 HTML 被缓存，key 含 post 的 cache_version &lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;%&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;%#&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; post 更新 → 版本变 → 旧缓存自动失效，无需手动清除 &lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;%&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;%=&lt;/span&gt; render @posts &lt;span style=&#34;color:#75715e&#34;&gt;%&amp;gt;&lt;/span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;%#&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; 配上 collection_cache 自动批量读缓存 &lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;%&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Comment&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;ApplicationRecord&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  belongs_to &lt;span style=&#34;color:#e6db74&#34;&gt;:post&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;touch&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;   &lt;span style=&#34;color:#75715e&#34;&gt;# 评论变化 → 触碰 post 时间戳 → 上层缓存联动失效&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;code&gt;touch: true&lt;/code&gt; + 片段缓存嵌套（评论缓存套文章缓存）就是著名的 Russian Doll Caching——失效问题通过「版本号联动」解决，思路和 CDN 的 cache-busting 一脉相承。缓存存储开发期用 &lt;code&gt;:memory_store&lt;/code&gt;，生产用 Redis 或 Solid Cache。&lt;/p&gt;
&lt;h3 id=&#34;63-action-mailer-与-active-storage&#34;&gt;6.3 Action Mailer 与 Active Storage&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;UserMailer&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;ApplicationMailer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;welcome&lt;/span&gt;(user)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    @user &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; user
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    mail &lt;span style=&#34;color:#e6db74&#34;&gt;to&lt;/span&gt;: user&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;email, &lt;span style=&#34;color:#e6db74&#34;&gt;subject&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;欢迎加入&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 视图就是 app/views/user_mailer/welcome.html.erb，和页面一样写&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;Active Storage 处理文件上传，一行 &lt;code&gt;has_one_attached :avatar&lt;/code&gt;，本地磁盘/S3/GCS 切换只改配置，图片变体（缩略图）懒生成：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;User&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;ApplicationRecord&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  has_one_attached &lt;span style=&#34;color:#e6db74&#34;&gt;:avatar&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;user&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;avatar&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;variant(&lt;span style=&#34;color:#e6db74&#34;&gt;resize_to_limit&lt;/span&gt;: &lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;200&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;200&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;64-concern-与代码组织胖-model-的出路&#34;&gt;6.4 Concern 与代码组织：胖 Model 的出路&lt;/h3&gt;
&lt;p&gt;Rails 应用三年后的典型病：User 模型两千行。标准疗法：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# app/models/concerns/searchable.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;module&lt;/span&gt; Searchable
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;extend&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;ActiveSupport&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;Concern&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  included &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    scope &lt;span style=&#34;color:#e6db74&#34;&gt;:search&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;(q) { where(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;title ILIKE ?&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;%&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;#{&lt;/span&gt;q&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%&amp;#34;&lt;/span&gt;) }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  class_methods &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;reindex_all&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; find_each(&lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;:reindex&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;再往上是 Service Object（&lt;code&gt;app/services/order_checkout.rb&lt;/code&gt;）封装跨模型的业务事务。原则：&lt;strong&gt;Concern 收「领域能力」，Service 收「业务流程」，Model 只留「数据与自身规则」&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第七篇-安全api-与测试敢交付的标准&#34;&gt;第七篇 安全、API 与测试：敢交付的标准&lt;/h2&gt;
&lt;h3 id=&#34;71-rails-自带的安全底盘知道它在保护你什么&#34;&gt;7.1 Rails 自带的安全底盘（知道它在保护你什么）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SQL 注入&lt;/strong&gt;：参数化查询是默认，危险的是你手写字符串拼接——&lt;code&gt;where(&amp;quot;name = &#39;#{params[:name]}&#39;&amp;quot;)&lt;/code&gt; 是自杀写法，永远用占位符。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;XSS&lt;/strong&gt;：ERB 默认转义一切输出，&lt;code&gt;&amp;lt;%= raw @html %&amp;gt;&lt;/code&gt; 和 &lt;code&gt;html_safe&lt;/code&gt; 是你在主动拆掉护栏。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSRF&lt;/strong&gt;：表单和 Turbo 请求自动带 token，无需操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mass Assignment&lt;/strong&gt;：必须懂的 Strong Parameters——&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;create&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  @post &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; current_user&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;posts&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;build(post_params)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;post_params&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  params&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;require(&lt;span style=&#34;color:#e6db74&#34;&gt;:post&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;permit(&lt;span style=&#34;color:#e6db74&#34;&gt;:title&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;:body&lt;/span&gt;)  &lt;span style=&#34;color:#75715e&#34;&gt;# 白名单，role/admin 字段进不来&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;72-认证别自己发明轮子&#34;&gt;7.2 认证：别自己发明轮子&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;常规产品：&lt;strong&gt;Devise&lt;/strong&gt;，注册/登录/找回密码/邮箱确认全家桶，二十年了还是标准答案。&lt;/li&gt;
&lt;li&gt;简单系统：Rails 自带的 &lt;code&gt;has_secure_password&lt;/code&gt; + 手写 session，代码量出奇的少，官方 8 代甚至内置了 authentication generator。&lt;/li&gt;
&lt;li&gt;授权（谁能不能做什么）用 &lt;strong&gt;Pundit&lt;/strong&gt; 或 CanCanCan，策略类清晰可测。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;73-写-api-给前端移动端&#34;&gt;7.3 写 API 给前端/移动端&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;rails new api_server --api    &lt;span style=&#34;color:#75715e&#34;&gt;# 纯 API 模式：砍掉视图/中间件，更轻&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;序列化用 &lt;code&gt;alba&lt;/code&gt; 或 &lt;code&gt;jbuilder&lt;/code&gt;（别用 AMS，已停更）；分页 &lt;code&gt;kaminari&lt;/code&gt; 或 &lt;code&gt;pagy&lt;/code&gt;（pagy 更快）；限流 Rack::Attack；版本管理走 URL 前缀 &lt;code&gt;namespace :v1&lt;/code&gt;。JSON 接口的其余部分和写页面几乎没区别——这又是 Rails 的甜头。&lt;/p&gt;
&lt;h3 id=&#34;74-测试rails-社区的肌肉记忆&#34;&gt;7.4 测试：Rails 社区的肌肉记忆&lt;/h3&gt;
&lt;p&gt;Rails 默认 Minitest，生态主流是 RSpec，选一个用熟（团队接手成本上 Minitest 更低，表达力上 RSpec 更好）。真正重要的不是选谁，是&lt;strong&gt;测什么&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;Model 测试 最多&amp;lt;br/&amp;gt;业务规则 校验 关联&amp;#34;] --&amp;gt; B[&amp;#34;Request/Integration 测试 适量&amp;lt;br/&amp;gt;接口契约 权限边界&amp;#34;]
    B --&amp;gt; C[&amp;#34;System 测试 少而精&amp;lt;br/&amp;gt;核心用户路径走浏览器&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;PostTest&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;ActiveSupport&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;TestCase&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  test &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;标题不能为空&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    post &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;Post&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;new(&lt;span style=&#34;color:#e6db74&#34;&gt;body&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;x&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    assert_not post&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;valid?
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    assert_includes post&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;errors&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;:title&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;can&amp;#39;t be blank&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  test &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;发布后生成 slug&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    post &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; posts(&lt;span style=&#34;color:#e6db74&#34;&gt;:draft&lt;/span&gt;)   &lt;span style=&#34;color:#75715e&#34;&gt;# fixtures 测试数据&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    post&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;publish!
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    assert_equal &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;hello-world&amp;#34;&lt;/span&gt;, post&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;slug
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;经验：Model 测试写到「改业务规则会红」的密度；System 测试只覆盖登录、下单这类断了要命的链路（它慢且脆）；CI 里 &lt;code&gt;bin/rails test&lt;/code&gt; 必须是合并门禁。&lt;/p&gt;
&lt;h3 id=&#34;75-调用外部-http-api你的应用也是别人的下游&#34;&gt;7.5 调用外部 HTTP API：你的应用也是别人的下游&lt;/h3&gt;
&lt;p&gt;7.3 是给别人写 API，这节反过来——调第三方支付、调内部服务、收 Webhook 回调。&lt;/p&gt;
&lt;p&gt;HTTP 客户端选 &lt;strong&gt;Faraday&lt;/strong&gt;（生态事实标准，中间件机制成熟），别在生产代码里裸用 &lt;code&gt;Net::HTTP&lt;/code&gt;（API 别扭、超时默认值坑多）；一次性脚本图省事可以 &lt;code&gt;HTTParty&lt;/code&gt;。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;conn &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;Faraday&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;new(&lt;span style=&#34;color:#e6db74&#34;&gt;url&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;https://api.pay.example.com&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt;f&lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  f&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;request  &lt;span style=&#34;color:#e6db74&#34;&gt;:json&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  f&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;response &lt;span style=&#34;color:#e6db74&#34;&gt;:json&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  f&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;response &lt;span style=&#34;color:#e6db74&#34;&gt;:raise_error&lt;/span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# 非 2xx 直接抛异常&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  f&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;options&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;open_timeout &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;     &lt;span style=&#34;color:#75715e&#34;&gt;# 连接超时：宁可快速失败&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  f&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;options&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;timeout      &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;     &lt;span style=&#34;color:#75715e&#34;&gt;# 读超时&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;resp &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; conn&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;post(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/v1/charges&amp;#34;&lt;/span&gt;, { &lt;span style=&#34;color:#e6db74&#34;&gt;amount&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;resp&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;body&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;三条纪律：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;超时必须显式设，连接/读分开&lt;/strong&gt;——不设超时的 HTTP 调用，等于把 Puma 线程抵押给对方的服务质量（为什么这会要命，下一篇并发模型讲）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;慢的、允许失败的调用放 Job 里&lt;/strong&gt;（§6.1）；请求-响应周期内只留「没它页面出不来」的调用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;收 Webhook 要验签&lt;/strong&gt;：支付平台的回调用 HMAC 签名验证（从 raw body 算摘要再比对），处理逻辑必须幂等——同一个事件会被重复推送。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;超时之后的重试、熔断、幂等设计，是接任何外部组件的通用纪律，进阶篇第五章专门展开；这里先守住「设超时」这条底线。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第八篇-生产部署从能跑到扛得住&#34;&gt;第八篇 生产部署：从能跑到扛得住&lt;/h2&gt;
&lt;h3 id=&#34;81-2026-年的标准姿势docker--kamal&#34;&gt;8.1 2026 年的标准姿势：Docker + Kamal&lt;/h3&gt;
&lt;p&gt;Rails 官方现在的部署答案是 &lt;strong&gt;Kamal&lt;/strong&gt;（前身 MRSK）：容器化打包，推到任意一台裸机/云主机，零停机滚动发布，不需要 Kubernetes。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Rails 8 新项目自带 Dockerfile 和 Kamal 配置&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;kamal setup      &lt;span style=&#34;color:#75715e&#34;&gt;# 首次：装 Docker、拉镜像、起服务、配证书&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;kamal deploy     &lt;span style=&#34;color:#75715e&#34;&gt;# 之后每次发布就这一条&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;code&gt;config/deploy.yml&lt;/code&gt; 里声明服务器、镜像仓库、环境变量（走 &lt;code&gt;.kamal/secrets&lt;/code&gt; 注入，密钥不进 git）。一台 4C8G 的云主机这么部署，扛住日活几万的 CRUD 产品没有压力——&lt;strong&gt;Rails 应用绝大多数死在数据库，不是死在框架&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;82-并发模型gvlpuma-进程线程与线程安全&#34;&gt;8.2 并发模型：GVL、Puma 进程×线程与线程安全&lt;/h3&gt;
&lt;p&gt;调 Puma 参数之前先理解 Ruby 的并发现实，否则配置全是瞎猜。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GVL（全局虚拟机锁）&lt;/strong&gt;：MRI Ruby 同一时刻只允许一个线程执行 Ruby 代码——听起来多线程是假的？不完全是：&lt;strong&gt;线程一旦进入阻塞 IO（查数据库、调 HTTP、读文件），就会释放 GVL 让其他线程跑&lt;/strong&gt;。Web 请求的大部分时间在等数据库和网络，所以多线程对 Rails 依然非常有效；CPU 密集型任务（图像处理、大报表生成）多线程才是真的没用，得靠多进程或扔进 Job。&lt;/p&gt;
&lt;p&gt;Puma 因此是&lt;strong&gt;进程×线程的混合模型&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    M[&amp;#34;Puma 主进程&amp;lt;br/&amp;gt;接请求、分发&amp;#34;] --&amp;gt; W1
    M --&amp;gt; W2
    subgraph W1[&amp;#34;Worker 1（独立进程，约 300MB 内存）&amp;#34;]
        T1[&amp;#34;Threads × N&amp;lt;br/&amp;gt;吃 IO 等待的红利&amp;#34;]
    end
    subgraph W2[&amp;#34;Worker 2&amp;#34;]
        T2[&amp;#34;Threads × N&amp;#34;]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;worker（进程）&lt;/strong&gt;：真正的并行单位，数量 ≈ CPU 核数，但每个 worker 独吞一份内存（200-400MB）。4C8G 的机器，&lt;code&gt;WEB_CONCURRENCY&lt;/code&gt; 一般 2~4，多了内存先爆。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;thread（线程）&lt;/strong&gt;：吃 IO 等待的红利，默认 5；IO 重的应用可以开到 10~16，再高收益递减（GVL 竞争 + 下游连接压力）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;连接池必须 ≥ 线程数&lt;/strong&gt;：&lt;code&gt;config/database.yml&lt;/code&gt; 的 &lt;code&gt;pool&lt;/code&gt; 默认对齐 &lt;code&gt;RAILS_MAX_THREADS&lt;/code&gt;。池子比线程小，高并发下请求会排队等连接甚至超时——Rails 生产事故的经典款。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;线程安全的纪律只有几条，但条条带血：&lt;strong&gt;别用类变量和全局可变状态&lt;/strong&gt;（&lt;code&gt;@@count&lt;/code&gt;、&lt;code&gt;$cache = ...&lt;/code&gt; 在多线程下互相踩踏）；请求级上下文用 Rails 自带的 &lt;code&gt;CurrentAttributes&lt;/code&gt;（请求结束自动清理，自己维护 &lt;code&gt;Thread.current&lt;/code&gt; 的十有八九会泄露给下一个请求）。Sidekiq 是纯线程模型（一个进程默认 5~25 线程），写 Job 代码时同样的纪律适用。&lt;/p&gt;
&lt;h3 id=&#34;83-生产检查清单&#34;&gt;8.3 生产检查清单&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Puma&lt;/strong&gt;：默认配置起步，worker 数按内存调（一个 worker 约 200-400MB），&lt;code&gt;WEB_CONCURRENCY&lt;/code&gt; 环境变量控制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据库&lt;/strong&gt;：PostgreSQL + 连接数规划（Puma 线程总数 ≤ PG max_connections，留余量给任务队列）；慢查询开 &lt;code&gt;log_min_duration_statement = 200&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;密钥&lt;/strong&gt;：&lt;code&gt;bin/rails credentials:edit&lt;/code&gt;（加密文件进 git，主密钥走环境变量），永远不要明文提交 secret。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日志&lt;/strong&gt;：生产环境 &lt;code&gt;config.logger&lt;/code&gt; 输出到 stdout，交给容器平台收集；想结构化日志上 &lt;code&gt;lograge&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;监控&lt;/strong&gt;：日志/指标/追踪三件套怎么搭，见 §8.5。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资产&lt;/strong&gt;：&lt;code&gt;config.public_file_server&lt;/code&gt; 配 CDN 或对象存储；Active Storage 生产必上 S3 系存储。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;备份&lt;/strong&gt;：数据库定时快照 + 异地副本，&lt;strong&gt;恢复演练每季度做一次&lt;/strong&gt;——没演练过的备份等于没有。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;84-cicd-最简闭环&#34;&gt;8.4 CI/CD 最简闭环&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# .github/workflows/ci.yml 骨架&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- &lt;span style=&#34;color:#f92672&#34;&gt;run&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;bin/rails db:setup&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- &lt;span style=&#34;color:#f92672&#34;&gt;run&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;bin/rails test&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- &lt;span style=&#34;color:#f92672&#34;&gt;run&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;bin/rails test:system&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- &lt;span style=&#34;color:#f92672&#34;&gt;run&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;bin/rubocop         &lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;# 代码风格统一&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- &lt;span style=&#34;color:#f92672&#34;&gt;run&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;bin/brakeman        &lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;# 安全扫描，Rails 专属&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;合并到 main 后触发 &lt;code&gt;kamal deploy&lt;/code&gt;，从提交到上线 10 分钟以内——这是小团队最舒服的节奏。&lt;/p&gt;
&lt;h3 id=&#34;85-监控与可观测性日志指标追踪三件套&#34;&gt;8.5 监控与可观测性：日志、指标、追踪三件套&lt;/h3&gt;
&lt;p&gt;Java 那边 SkyWalking/Arthas 一套搞定的事，Rails 世界分层选型：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;错误追踪&lt;/strong&gt;：Sentry（&lt;code&gt;sentry-ruby&lt;/code&gt;）——异常自动上报，带堆栈、请求上下文、release 关联，小团队起步首选；同类还有 Honeybadger/Rollbar。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;APM（SkyWalking 调用链的对应物）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rails 专属&lt;/strong&gt;：Skylight / Scout——endpoint 级耗时分解（视图、SQL、外部调用各占多少）、自动揪 N+1、慢查询定位，接入就是一个 gem。业务以 Rails 为主时体验好过通用方案。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通用标准&lt;/strong&gt;：OpenTelemetry（&lt;code&gt;opentelemetry-instrumentation-rails&lt;/code&gt;）——分布式追踪，把一次请求在 Rails、Sidekiq、下游 HTTP 之间的调用链串成一条 trace。公司已有 SkyWalking 的话不用换生态：SkyWalking 后端原生支持 OTLP 协议接收，OTel 的 trace 直接发过去就行（SkyWalking 自己没有维护好的 Ruby agent，OTLP 就是那座桥）。外部调用的埋点写法见进阶篇 §5.6。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;指标（流量监控）&lt;/strong&gt;：Prometheus + &lt;code&gt;prometheus_exporter&lt;/code&gt;（或 &lt;code&gt;yabeda&lt;/code&gt; 系列 gem），Grafana 出面板。必看四个数：&lt;strong&gt;QPS、p95/p99 延迟、5xx 率、Sidekiq 队列长度&lt;/strong&gt;——队列积压是最灵敏的故障先行指标，比 5xx 报警早几分钟到几十分钟。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;日志&lt;/strong&gt;：生产 stdout + &lt;code&gt;lograge&lt;/code&gt; 结构化（一行一个 JSON），交 Loki/ELK 收集；&lt;code&gt;config.log_tags = [:request_id]&lt;/code&gt; 给每条日志带上请求 ID，一次请求的所有日志才能串起来。&lt;/p&gt;
&lt;p&gt;最后提一个白送的：Rails 自带 &lt;code&gt;/up&lt;/code&gt; 健康检查端点，给 LB/K8s 探针用，别自己造。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;收尾精通之路在心法不在-api&#34;&gt;收尾：精通之路在心法不在 API&lt;/h2&gt;
&lt;p&gt;八篇走完，你已经具备交付一个生产级 Rails 应用的全部知识。最后留几条只有踩过坑才会信的心法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;待在主路上（The Rails Way）&lt;/strong&gt;。Rails 的约定是几千个项目熬出来的共识。每次你想「我偏要按我的方式组织代码」，先问自己是真有特殊性，还是只是不习惯——九成的「不习惯」三个月后都会变成「真香」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;魔法要能用一句话解释&lt;/strong&gt;。&lt;code&gt;belongs_to&lt;/code&gt; 干了什么、&lt;code&gt;resources&lt;/code&gt; 生成了什么路由、Turbo Stream 发了什么——能解释就是工具，不能解释就是债务。卡住时 &lt;code&gt;rails console&lt;/code&gt; 里 &lt;code&gt;method(:x).source_location&lt;/code&gt; 直接跳源码，Rails 源码的可读性本身就是最好的进阶教材。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;先交付，再优化&lt;/strong&gt;。Rails 的性能争议大多是「没做缓存、没加索引、N+1 满天飞」的锅，不是框架的锅。按第四篇和第六篇的方法把基础卫生做好，Rails 撑到你需要考虑重构的那天绰绰有余。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI 时代 Rails 学习者的红利&lt;/strong&gt;：因为约定极强，AI 生成的 Rails 代码风格高度一致、可直接运行率远高于其他框架。你的核心竞争力不再是「会写」，而是「看得懂生成的代码对不对」——这恰恰要求你对 MVC 骨架和约定理解得更扎实，而不是更浅。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;工具会换代，「约定优于配置」「不要重复自己」「快速交付价值」这些思想不会。祝 coding 愉快。&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
