<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Rails on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/rails/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Mon, 14 Sep 2026 14:00:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/rails/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>Ruby on Rails 高级篇：接入 Redis、Elasticsearch、消息队列与大数据组件的工程实践</title>
      <link>https://ftmao.com/blog/posts/ruby-on-rails-advanced-external-components/</link>
      <pubDate>Mon, 14 Sep 2026 14:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/ruby-on-rails-advanced-external-components/</guid>
      <description>&lt;h1 id=&#34;ruby-on-rails-高级篇接入-rediselasticsearch消息队列与大数据组件的工程实践&#34;&gt;Ruby on Rails 高级篇：接入 Redis、Elasticsearch、消息队列与大数据组件的工程实践&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;姊妹篇：《&lt;a href=&#34;https://ftmao.com/blog/blog/posts/ruby-on-rails-beginner-to-mastery/&#34;&gt;Ruby on Rails 从零基础到精通&lt;/a&gt;》——入门篇讲「把功能写出来」，这一篇讲「把系统接出去」。
目标读者：已经能独立写 Rails CRUD，现在产品要接搜索、接缓存、接消息队列、接大数据管道，想知道「正确姿势」而不是「能跑就行」的开发者。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;入门篇的应用是自包含的：一个 Rails 进程 + 一个数据库就是全世界。真实系统不是这样——搜索要 ES、缓存要 Redis、异步要 MQ、报表要大数据平台。每接一个组件，就多一种故障模式。这一篇的结构：&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;一 Redis&amp;lt;br/&amp;gt;缓存 队列 数据结构&amp;#34;] --&amp;gt; B[&amp;#34;二 Elasticsearch&amp;lt;br/&amp;gt;搜索与索引同步&amp;#34;]
    B --&amp;gt; C[&amp;#34;三 消息队列&amp;lt;br/&amp;gt;RabbitMQ / Kafka&amp;#34;]
    C --&amp;gt; D[&amp;#34;四 大数据组件&amp;lt;br/&amp;gt;Spark / Flink / ClickHouse&amp;#34;]
    D --&amp;gt; E[&amp;#34;五 通用纪律&amp;lt;br/&amp;gt;超时 重试 熔断 幂等&amp;#34;]
    style E fill:#e8f4e8&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;前四篇每个组件都回答三个问题：&lt;strong&gt;用什么 gem、数据怎么流动、生产上死在哪&lt;/strong&gt;。第五篇是把前四篇的教训抽象成通用清单——以后接任何新组件，照着过一遍就行。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第一篇-redis一个组件四种角色&#34;&gt;第一篇 Redis：一个组件，四种角色&lt;/h2&gt;
&lt;p&gt;Rails 接 Redis 最大的误区是「装个 redis gem 存取字符串」。Redis 在 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 TD
    R[(&amp;#34;Redis&amp;#34;)]
    R1[&amp;#34;角色1 缓存&amp;lt;br/&amp;gt;Rails.cache&amp;#34;] --- R
    R2[&amp;#34;角色2 任务队列&amp;lt;br/&amp;gt;Sidekiq&amp;#34;] --- R
    R3[&amp;#34;角色3 实时推送&amp;lt;br/&amp;gt;Action Cable&amp;#34;] --- R
    R4[&amp;#34;角色4 业务数据结构&amp;lt;br/&amp;gt;计数器/排行榜/锁&amp;#34;] --- R&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&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;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/environments/production.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;config&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;cache_store &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;:redis_cache_store&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;url&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;ENV&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;REDIS_CACHE_URL&amp;#34;&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;connect_timeout&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;,     &lt;span style=&#34;color:#75715e&#34;&gt;# 连接超时 1s，别用默认的 5s&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;read_timeout&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 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 style=&#34;color:#e6db74&#34;&gt;write_timeout&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 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 style=&#34;color:#e6db74&#34;&gt;error_handler&lt;/span&gt;: &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;(method:, &lt;span style=&#34;color:#e6db74&#34;&gt;returning&lt;/span&gt;:, &lt;span style=&#34;color:#e6db74&#34;&gt;exception&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;# 关键：Redis 挂了降级为「缓存 miss」，而不是整个请求 500&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;Sentry&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;capture_exception(exception)
&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&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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;之后入门篇第六篇讲的 &lt;code&gt;Rails.cache.fetch&lt;/code&gt;、俄罗斯套娃缓存全部自动走 Redis。两个生产级要点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;error_handler 必须配&lt;/strong&gt;。缓存的定位是加速器，Redis 抖动不该打垮主站——miss 只是变慢，异常才是事故。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存和队列用不同的 Redis 实例/库&lt;/strong&gt;。缓存实例可以随便清、可以开 &lt;code&gt;allkeys-lru&lt;/code&gt; 淘汰；队列实例丢数据就是丢任务。混在一个实例上，运维策略互相打架。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;三大经典问题的 Rails 解法：&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;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;穿透（查不存在的数据）&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;Rails.cache.fetch&lt;/code&gt; 缓存 nil（&lt;code&gt;skip_nil: false&lt;/code&gt; 是默认行为，nil 也会被缓存），恶意枚举 ID 时考虑布隆过滤器&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;击穿（热点 key 过期被并发回源）&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;Rails.cache.fetch(key, race_condition_ttl: 10)&lt;/code&gt;——过期后第一个进程回源，其余继续用旧值撑 10 秒&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;雪崩（大面积同时过期）&lt;/td&gt;
					&lt;td&gt;TTL 加随机抖动：&lt;code&gt;expires_in: 30.minutes + rand(300).seconds&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;12-角色二sidekiq-任务队列&#34;&gt;1.2 角色二：Sidekiq 任务队列&lt;/h3&gt;
&lt;p&gt;入门篇讲过 ActiveJob 的 API，生产上吞吐量要求高的场景底层换成 Sidekiq：&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/application.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;config&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;active_job&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;queue_adapter &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;:sidekiq&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-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;# config/sidekiq.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;:queues&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:#ae81ff&#34;&gt;critical, 3]&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:#ae81ff&#34;&gt;default, 2]&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:#ae81ff&#34;&gt;low, 1]&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;Sidekiq 与 ActiveJob 的关系：ActiveJob 是 Rails 的标准接口（可换后端），Sidekiq 是高性能实现。建议业务代码只写 ActiveJob API，但&lt;strong&gt;有两件事只有原生 Sidekiq 做得好&lt;/strong&gt;：定时任务（sidekiq-cron / sidekiq-scheduler）和中间件链（统一埋点、限流）。队列 Redis 的内存要按「任务堆积三天不丢」规划，持久化开 AOF。&lt;/p&gt;
&lt;h3 id=&#34;13-角色三action-cable-实时推送&#34;&gt;1.3 角色三：Action Cable 实时推送&lt;/h3&gt;
&lt;p&gt;入门篇的 Turbo Streams 底层就是 Action Cable，默认用 Redis pub/sub 做进程间广播：&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-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;# config/cable.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;production&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;adapter&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;redis&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;url&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;&amp;lt;%= ENV.fetch(&amp;#34;REDIS_CABLE_URL&amp;#34;) %&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;多实例部署时这个是必须的（甲请求打到实例 A，乙的浏览器连着实例 B，靠 Redis 广播互通）。流量大了可以换 &lt;code&gt;anycable&lt;/code&gt;（Go 实现，单机连接数高一个数量级），Rails 代码不用改。&lt;/p&gt;
&lt;h3 id=&#34;14-角色四业务数据结构&#34;&gt;1.4 角色四：业务数据结构&lt;/h3&gt;
&lt;p&gt;排行榜、计数器、分布式锁、去重集合——这些直接用 Redis 数据结构，比塞进 MySQL 合适：&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;# Kredis：Rails 官方出品的 Redis 数据结构封装&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;  kredis_list &lt;span style=&#34;color:#e6db74&#34;&gt;:recent_viewer_ids&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;limit&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;  kredis_counter &lt;span style=&#34;color:#e6db74&#34;&gt;:views&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  kredis_unique_list &lt;span style=&#34;color:#e6db74&#34;&gt;:tag_cache&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;limit&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;50&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;views&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;increment                    &lt;span style=&#34;color:#75715e&#34;&gt;# INCR，原子计数&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;recent_viewer_ids&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;prepend(user&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;id) &lt;span style=&#34;color:#75715e&#34;&gt;# 自动截断到 100&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;redis-mutex&lt;/code&gt; 或 Sidekiq 生态的 &lt;code&gt;sidekiq-unique-jobs&lt;/code&gt;（防止同一任务并发执行，这是任务系统最常用的锁场景）。&lt;/p&gt;
&lt;h3 id=&#34;15-追问有没有-jetcache-那种本地--redis两级缓存&#34;&gt;1.5 追问：有没有 JetCache 那种「本地 + Redis」两级缓存？&lt;/h3&gt;
&lt;p&gt;Java 的 JetCache/多层缓存方案（Caffeine 进程内 + Redis 远端 + pub/sub 广播失效）在 Ruby 生态&lt;strong&gt;没有对等的成熟 gem&lt;/strong&gt;，原因在部署模型：Java 是单进程多线程，Caffeine 缓存被整个 JVM 共享，L1 收益巨大；Ruby 应用服务器是多进程模型（Puma 5 个 worker = 5 个独立进程，内存互不相通），进程内缓存每个 worker 各存一份，命中率被摊薄、失效还得跨进程广播——收益/复杂度比不划算，生态便没有收编出「JetCache.rb」。&lt;/p&gt;
&lt;p&gt;Rails 的现实替代分三层：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;请求级 L1（内建，你已经在用）&lt;/strong&gt;：所有 &lt;code&gt;ActiveSupport::Cache&lt;/code&gt; store 都内置 &lt;code&gt;LocalCache&lt;/code&gt; 策略——同一次请求内重复读同一个 key 走进程内存，不打 Redis，请求结束自动清空，无一致性问题。它解决的是「一次请求内 N+1 次读缓存」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨请求 L1（手卷，二十来行）&lt;/strong&gt;：配置/字典类「变很少、读极多」的数据，用 &lt;code&gt;Concurrent::Map&lt;/code&gt; + 短 TTL + Redis pub/sub 主动失效：&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;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ConfigCache&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;TTL&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;30&lt;/span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;# 短 TTL 兜底：pub/sub 丢消息最多脏 30s&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;self&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(key)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    entry &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; store&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;key&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;return&lt;/span&gt; entry&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;:value&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; entry &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; entry&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;:exp&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&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;now
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    store&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;key&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; { &lt;span style=&#34;color:#e6db74&#34;&gt;value&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;yield&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;exp&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;TTL&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;seconds&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;from_now }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    store&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;key&lt;span style=&#34;color:#f92672&#34;&gt;][&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;:value&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;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;self&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;store&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (@store &lt;span style=&#34;color:#f92672&#34;&gt;||=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;Concurrent&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;Map&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;new)
&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;# 启动时订阅失效广播（多 worker 各自订阅）&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;self&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;listen&lt;/span&gt;(redis)
&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;Thread&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;new { redis&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;subscribe(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;config:invalidate&amp;#34;&lt;/span&gt;) { &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt;m&lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; store&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;delete(m&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;message) } }
&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;ul&gt;
&lt;li&gt;&lt;strong&gt;Rails 8 的官方答案：Solid Cache 用本地 NVMe 当「超大 L1」&lt;/strong&gt;：与其在内存里抠几千条 entry，不如把缓存放本机 SSD（SQLite 本地文件）——容量大两个数量级、无网络往返、持久化重启不丢。思路是不优化单次读取延迟，而是把「几乎什么都缓存」变便宜。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第二篇-elasticsearch搜索是数据同步问题&#34;&gt;第二篇 Elasticsearch：搜索是数据同步问题&lt;/h2&gt;
&lt;p&gt;Rails 接 ES，写查询语法是简单的一半，&lt;strong&gt;难的一半是「MySQL 里的数据怎么进 ES、怎么保持一致」&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;21-gem-选型&#34;&gt;2.1 Gem 选型&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Gem&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;Searchkick&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;封装好、中文分词/高亮/聚合开箱即用&lt;/td&gt;
					&lt;td&gt;大多数项目的首选&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;elasticsearch-model&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;官方 gem，贴近原生 DSL&lt;/td&gt;
					&lt;td&gt;查询复杂、需要完全控制力时&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;以 Searchkick 为例：&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;  searchkick &lt;span style=&#34;color:#e6db74&#34;&gt;word_middle&lt;/span&gt;: &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:#75715e&#34;&gt;# title 支持中间匹配（类 LIKE %x%）&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;settings&lt;/span&gt;: { &lt;span style=&#34;color:#e6db74&#34;&gt;number_of_shards&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;1&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;# 控制哪些字段进 ES、以什么形态进&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;search_data&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:#e6db74&#34;&gt;title&lt;/span&gt;: title,
&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;body&lt;/span&gt;: body,
&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;user_name&lt;/span&gt;: user&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;name,      &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;published_at&lt;/span&gt;: published_at,
&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;comment_count&lt;/span&gt;: comments&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;size
&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;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;# 中文分词：ES 集群装 IK 插件后&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;# Post.search &amp;#34;视频&amp;#34;, fields: [:title], 自动走配置的分析器&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;search &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ruby 教程&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;where&lt;/span&gt;: { &lt;span style=&#34;color:#e6db74&#34;&gt;published_at&lt;/span&gt;: { &lt;span style=&#34;color:#e6db74&#34;&gt;lt&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:#e6db74&#34;&gt;order&lt;/span&gt;: { &lt;span style=&#34;color:#e6db74&#34;&gt;comment_count&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:#e6db74&#34;&gt;page&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 style=&#34;color:#e6db74&#34;&gt;per_page&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;20&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;search_data&lt;/code&gt; 是核心设计点：&lt;strong&gt;ES 文档是反范式的&lt;/strong&gt;。关联表的字段（作者名、评论数）要打平进文档，否则查询时要回库 join，ES 的意义就没了。代价是：用户名改了，所有相关文档都要重建索引——这就是下一节的问题。&lt;/p&gt;
&lt;h3 id=&#34;22-索引同步三种策略的取舍&#34;&gt;2.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;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    W[&amp;#34;业务写入 MySQL&amp;#34;] --&amp;gt; S1[&amp;#34;策略A Model 回调&amp;lt;br/&amp;gt;同步写 ES&amp;#34;]
    W --&amp;gt; S2[&amp;#34;策略B 后台 Job&amp;lt;br/&amp;gt;异步写 ES&amp;#34;]
    W --&amp;gt; S3[&amp;#34;策略C CDC&amp;lt;br/&amp;gt;binlog 监听管道&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;策略 A：回调同步写&lt;/strong&gt;（Searchkick 默认 &lt;code&gt;after_commit&lt;/code&gt;）。简单强一致，但 ES 抖动直接拖慢/打挂业务写入，只适合内部系统。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;策略 B：回调只入队，Job 异步写&lt;/strong&gt;（&lt;code&gt;Searchkick&lt;/code&gt; 配置 &lt;code&gt;Searchkick.callbacks = :async&lt;/code&gt;，或自写 &lt;code&gt;after_commit { ReindexJob.perform_later(self) }&lt;/code&gt;）。&lt;strong&gt;大多数项目的正确选择&lt;/strong&gt;：写入不被 ES 拖累，失败可重试。注意 Job 里要重新从 DB 读数据再写 ES（防止任务执行时数据已过期，写入陈旧版本）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;策略 C：CDC 管道&lt;/strong&gt;（MySQL binlog → Canal/Debezium → Kafka → 消费写 ES）。业务代码零侵入，适合写入量大、文档来源多的大系统，代价是多维护一条数据管道。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;23-零停机重建索引&#34;&gt;2.3 零停机重建索引&lt;/h3&gt;
&lt;p&gt;Mapping 变更（加字段、改分词）必须重建索引。标准流程靠&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:#75715e&#34;&gt;# 1. 建新索引 posts_v2，全量灌数据&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;reindex  &lt;span style=&#34;color:#75715e&#34;&gt;# Searchkick 自动建带时间戳的新索引并切换别名&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;# 底层原理：读永远走别名 posts → 指向 posts_v1 或 posts_v2&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;# reindex 完成后别名原子切换，全程无读中断&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;Searchkick 的 &lt;code&gt;reindex&lt;/code&gt; 默认就是这个模式，手工玩原生 API 时记住同一原则：&lt;strong&gt;读别名，不写别名；切换原子操作&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;24-生产上死在哪&#34;&gt;2.4 生产上死在哪&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;写放大&lt;/strong&gt;：热门对象高频更新（计数器、状态），每次都触发 reindex → 把高频字段拆出搜索文档，或改用 &lt;code&gt;update&lt;/code&gt; 部分更新。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;深分页&lt;/strong&gt;：&lt;code&gt;from + size&lt;/code&gt; 超过 1 万直接报错，列表导出场景用 &lt;code&gt;scroll&lt;/code&gt; 或 &lt;code&gt;search_after&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ES 当主库用&lt;/strong&gt;：ES 不是数据库——近实时（默认 1s 刷新）、无事务。写后立刻要读的场景，读 MySQL 兜底，别调 &lt;code&gt;refresh&lt;/code&gt; 硬扛。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第三篇-消息队列activejob-不是-mq&#34;&gt;第三篇 消息队列：ActiveJob 不是 MQ&lt;/h2&gt;
&lt;p&gt;先澄清一个普遍误解：&lt;strong&gt;ActiveJob/Sidekiq 是「任务队列」，不是「消息队列」&lt;/strong&gt;。两者的区别决定了选型：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;&lt;/th&gt;
					&lt;th&gt;任务队列（Sidekiq）&lt;/th&gt;
					&lt;th&gt;消息队列（RabbitMQ/Kafka）&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;语义&lt;/td&gt;
					&lt;td&gt;「替我执行这段代码」&lt;/td&gt;
					&lt;td&gt;「广播一个事实，谁关心谁消费」&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;消费方&lt;/td&gt;
					&lt;td&gt;只有自己系统的 worker&lt;/td&gt;
					&lt;td&gt;可以是任意多个异构系统&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;典型场景&lt;/td&gt;
					&lt;td&gt;发邮件、生成报表&lt;/td&gt;
					&lt;td&gt;订单已支付 → 通知库存/物流/积分/大数据&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Rails 内部异步用 ActiveJob 就够了；&lt;strong&gt;当事件的消费者不止你的 Rails 应用&lt;/strong&gt;（要对接 Java 服务、数据团队、第三方），才需要真 MQ。&lt;/p&gt;
&lt;h3 id=&#34;31-rabbitmq可靠投递优先&#34;&gt;3.1 RabbitMQ：可靠投递优先&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;# bunny 是 Ruby 的 RabbitMQ 客户端&lt;/span&gt;
&lt;/span&gt;&lt;/span&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;Bunny&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;new(&lt;span style=&#34;color:#66d9ef&#34;&gt;ENV&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;RABBITMQ_URL&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;tap(&lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;:start&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ch &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; conn&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;create_channel
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;exchange &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; ch&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;topic(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;orders&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;durable&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;&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;exchange&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;  { &lt;span style=&#34;color:#e6db74&#34;&gt;order_id&lt;/span&gt;: order&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;id, &lt;span style=&#34;color:#e6db74&#34;&gt;event&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;paid&amp;#34;&lt;/span&gt; }&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;to_json,
&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;routing_key&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order.paid&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;persistent&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:#e6db74&#34;&gt;message_id&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;SecureRandom&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;uuid   &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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;消费侧用 &lt;code&gt;sneakers&lt;/code&gt;（基于 bunny 的 worker 框架）。RabbitMQ 的关键概念是 exchange/routing key/queue 三层路由，灵活但概念多；适合「企业内部可靠事件通知」。&lt;/p&gt;
&lt;h3 id=&#34;32-kafka吞吐与回放优先&#34;&gt;3.2 Kafka：吞吐与回放优先&lt;/h3&gt;
&lt;p&gt;Rails 社区接 Kafka 的标准答案是 &lt;strong&gt;Karafka&lt;/strong&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;# app/consumers/order_events_consumer.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;OrderEventsConsumer&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;ApplicationConsumer&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;consume&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    messages&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;message&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;      event &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;JSON&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;parse(message&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;payload)
&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;OrderEventHandler&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;call(event)
&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:#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;# karafka.rb —— 声明消费哪个 topic&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#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;  topic &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_events&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;    consumer &lt;span style=&#34;color:#66d9ef&#34;&gt;OrderEventsConsumer&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;code&gt;WaterDrop&lt;/code&gt;（Karafka 配套的 producer）：&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;WaterDrop&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;SyncProducer&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;call(
&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;event&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order.paid&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;order_id&lt;/span&gt;: order&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;id }&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;to_json,
&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;topic&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_events&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;key&lt;/span&gt;: order&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;id&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;to_s   &lt;span style=&#34;color:#75715e&#34;&gt;# 同 key 进同分区 → 同一订单的事件保序&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;p&gt;Kafka 相比 RabbitMQ 的独特价值：&lt;strong&gt;消息可回放&lt;/strong&gt;（消费位点重置就能重跑历史事件）和&lt;strong&gt;分区保序&lt;/strong&gt;。大数据团队要全量订单事件时，只有 Kafka 这条路合适。&lt;/p&gt;
&lt;h3 id=&#34;33-消费侧的三个生死问题&#34;&gt;3.3 消费侧的三个生死问题&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;幂等&lt;/strong&gt;：MQ 的投递语义是「至少一次」，重复消费是常态不是异常。每个消费者都必须能安全重放：&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;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;call&lt;/span&gt;(event)
&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;# 用 message_id 去重，或用业务唯一键 upsert&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;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;ProcessedEvent&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;exists?(&lt;span style=&#34;color:#e6db74&#34;&gt;message_id&lt;/span&gt;: event&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;message_id&amp;#34;&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;ActiveRecord&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;Base&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;    &lt;span style=&#34;color:#66d9ef&#34;&gt;ProcessedEvent&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;create!(&lt;span style=&#34;color:#e6db74&#34;&gt;message_id&lt;/span&gt;: event&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;message_id&amp;#34;&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;    handle(event)
&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;ol start=&#34;2&#34;&gt;
&lt;li&gt;&lt;strong&gt;死信&lt;/strong&gt;：处理失败不能无限重试（一条坏消息卡死整个分区）。重试 N 次后进死信队列，告警 + 人工/延时处理。Karafka 有 Dead Letter Queue 原生支持，RabbitMQ 配 DLX。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发布可靠性&lt;/strong&gt;：「先写库后发消息」存在窗口期——库写了、进程挂了、消息没发。解法是 &lt;strong&gt;Outbox 模式&lt;/strong&gt;：业务数据和 outbox 事件表在同一个事务里写库，后台进程轮询 outbox 表投递 MQ，投递成功标记已发。数据一致性要求高的链路（钱、库存）必须上 outbox，不能裸发。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第四篇-大数据组件先想清楚-rails-的角色&#34;&gt;第四篇 大数据组件：先想清楚 Rails 的角色&lt;/h2&gt;
&lt;p&gt;标题里最常见的认知偏差是「Rails 怎么接 Spark」。正确的问题其实是：&lt;strong&gt;在大数据架构里，Rails 应用应该站在什么位置？&lt;/strong&gt; 答案是——Rails 几乎从不「接」Spark，它站在管道的两端：&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;Rails 应用&amp;lt;br/&amp;gt;业务数据库 MySQL/PG&amp;#34;] --&amp;gt;|&amp;#34;CDC / 事件 Kafka / 定时导出&amp;#34;| B[&amp;#34;数据管道&amp;lt;br/&amp;gt;Kafka&amp;#34;]
    B --&amp;gt; C[&amp;#34;计算引擎&amp;lt;br/&amp;gt;Spark / Flink&amp;#34;]
    C --&amp;gt; D[&amp;#34;分析存储&amp;lt;br/&amp;gt;ClickHouse / ES / Hive&amp;#34;]
    D --&amp;gt;|&amp;#34;API / 只读库 / 回写&amp;#34;| E[&amp;#34;Rails 应用&amp;lt;br/&amp;gt;展示分析结果&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;41-上游角色把数据交出去&#34;&gt;4.1 上游角色：把数据交出去&lt;/h3&gt;
&lt;p&gt;Rails 是业务数据的源头，交出去的方式按实时性递增：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定时导出&lt;/strong&gt;（小时/天级）：&lt;code&gt;find_each&lt;/code&gt; 分批读库导 CSV/Parquet 上传对象存储。简单可靠，报表场景够用。注意 &lt;code&gt;find_each&lt;/code&gt; 默认按主键分批，千万别在导大表时用 &lt;code&gt;each&lt;/code&gt;（全量加载内存爆炸）或 &lt;code&gt;offset&lt;/code&gt; 分页（越深越慢）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kafka 事件&lt;/strong&gt;（秒级）：重要业务事件按第三篇的方式发 Kafka，数据团队自己消费。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CDC&lt;/strong&gt;（准实时、全量变更）：Debezium/Canal 监听 binlog，业务代码零侵入。数据团队的最爱——不用求你在每个 Model 里埋事件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;原则：同一份数据只选一条路&lt;/strong&gt;，既发 Kafka 又跑 CDC 会造成数据团队收到双份口径。&lt;/p&gt;
&lt;h3 id=&#34;42-下游角色把结果拿回来&#34;&gt;4.2 下游角色：把结果拿回来&lt;/h3&gt;
&lt;p&gt;Spark/Flink 算完的结果（用户画像、推荐列表、统计报表），Rails 消费的方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;读分析库&lt;/strong&gt;：ClickHouse/ES 直接开只读连接给 Rails（用 &lt;code&gt;establish_connection&lt;/code&gt; 配第二数据源），OLAP 查询走它，别打 MySQL。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;回写业务库&lt;/strong&gt;：推荐结果、用户分群这类要参与业务逻辑的，由数据管道回写 MySQL/Redis，Rails 无感知地读——&lt;strong&gt;首选方案，耦合最低&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内部 HTTP API&lt;/strong&gt;：数据团队提供服务接口，Rails 用 Faraday/HTTParty 调。适用跨团队边界，记得按第五篇配超时熔断。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;43-反模式警告&#34;&gt;4.3 反模式警告&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;不要在 Rails 进程里嵌 Spark 客户端跑计算——Ruby 不在 JVM 大数据生态里，硬接只有痛苦。&lt;/li&gt;
&lt;li&gt;不要让 Spark 作业直接写 Rails 的在线 MySQL 主库——批量回写打爆主库的事故年年有，回写写从库同步的旁表或走限速的 API。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第五篇-通用纪律接入任何组件前的检查清单&#34;&gt;第五篇 通用纪律：接入任何组件前的检查清单&lt;/h2&gt;
&lt;p&gt;前四篇的教训抽象出来，是一套与具体组件无关的纪律。以后接任何外部服务（HTTP API、对象存储、AI 模型接口都一样），按这张清单过一遍：&lt;/p&gt;
&lt;h3 id=&#34;51-超时永远不依赖默认值&#34;&gt;5.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:#75715e&#34;&gt;# HTTP 客户端：连接/读/写三个超时分开设&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;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.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;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;3&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;open_timeout &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:#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;默认超时几乎都是灾难（Ruby 的 Net::HTTP 读超时默认 60s——下游挂了，你的 Puma 线程全部吊死 60 秒，线程池耗尽，全站 500）。&lt;strong&gt;所有跨进程调用必须显式设超时，且要比上游网关的超时短&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;52-重试只对幂等操作且带退避&#34;&gt;5.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;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;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;重试放大故障——下游已经过载，你的重试是在 DDoS 它。规则：GET/幂等操作才可重试；次数有上限；间隔指数退避加抖动。&lt;/p&gt;
&lt;h3 id=&#34;53-熔断下游持续失败就快速失败&#34;&gt;5.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:#75715e&#34;&gt;# stoplight gem：失败率超阈值 → 熔断器打开，直接抛错不发起请求&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;light &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;Stoplight&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;es-search&amp;#34;&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;search(query) }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;light&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;with_fallback { &lt;span style=&#34;color:#66d9ef&#34;&gt;Post&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;title LIKE ?&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;query&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:#f92672&#34;&gt;.&lt;/span&gt;limit(&lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt;) }  &lt;span style=&#34;color:#75715e&#34;&gt;# 降级：回退 DB 模糊查&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;run&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;features.es_search = false&lt;/code&gt; 时直接走 DB），是大流量系统的标准配置。&lt;/p&gt;
&lt;h3 id=&#34;54-幂等写操作带唯一键&#34;&gt;5.4 幂等：写操作带唯一键&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;对外发消息：带 &lt;code&gt;message_id&lt;/code&gt;（见第三篇）。&lt;/li&gt;
&lt;li&gt;对外调写 API：带 &lt;code&gt;Idempotency-Key&lt;/code&gt; header（Stripe 系 API 的标准）。&lt;/li&gt;
&lt;li&gt;自己提供写接口：用唯一索引兜底（&lt;code&gt;add_index :payments, :idempotency_key, unique: true&lt;/code&gt;），重复请求撞索引返回原结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;55-连接池与背压&#34;&gt;5.5 连接池与背压&lt;/h3&gt;
&lt;p&gt;每个组件的连接池都要和 Puma 并发模型对齐：Puma 5 workers × 16 threads = 80 并发 → DB/Redis 连接池至少 80 → 而下游服务/数据库能不能扛 80 个长连接，要算。Sidekiq 的并发（默认 5~25）单独算一份。连接数规划错位是生产事故的高发区。&lt;/p&gt;
&lt;h3 id=&#34;56-可观测性每次外部调用都埋点&#34;&gt;5.6 可观测性：每次外部调用都埋点&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;# 用 instrument 包裹外部调用，自动记录耗时，可接入 StatsD/Prometheus&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;ActiveSupport&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;Notifications&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;instrument(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;external.es_search&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;index&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;posts&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;  &lt;span style=&#34;color:#66d9ef&#34;&gt;Post&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;search(query)
&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;加上 SkyWalking/OpenTelemetry 做分布式追踪（Rails 侧 &lt;code&gt;opentelemetry-instrumentation-rails&lt;/code&gt; 一行接入），每个外部组件的耗时、错误率、QPS 都要能在监控面板上单独看到——出问题时你才知道是哪个组件在拖。&lt;/p&gt;
&lt;h3 id=&#34;57-生态边界不是所有中间件都有-ruby-版&#34;&gt;5.7 生态边界：不是所有中间件都有 Ruby 版&lt;/h3&gt;
&lt;p&gt;接入任何组件前先问一句：&lt;strong&gt;这是本生态的活法吗？&lt;/strong&gt; 最典型的例子是 Nacos/Sentinel 这类 Spring Cloud 全家桶——Nacos 官方只维护 Java/Go/Python 客户端，Ruby 没有可用 SDK。这不是生态缺陷，而是因为它们解决的问题在 Rails 世界各有归宿：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;服务发现&lt;/strong&gt;：Nacos 是客户端发现模式（Feign 需要实例列表），Rails 文化是单体优先；即使微服务化，K8s Service DNS / 云 LB 这类&lt;strong&gt;服务端发现&lt;/strong&gt;已把注册中心吃掉了，调用方根本不需要知道实例列表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置中心&lt;/strong&gt;：部署期配置用 ENV + ConfigMap，密钥用 credentials，动态开关用 &lt;code&gt;flipper&lt;/code&gt;，业务热配置一张 config 表加短 TTL 缓存就够。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;限流降级&lt;/strong&gt;：Sentinel 没有 Ruby 版，对应物是 Rack::Attack（限流）+ stoplight（熔断）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果公司基础设施已经是 Nacos（Rails 服务必须融入），原则是&lt;strong&gt;让平台层消化异构，不要在应用里养半成品客户端&lt;/strong&gt;：优先走 K8s 部署 + 网关适配（Rails 应用完全无感知）；必须注册进 Nacos 时，用 OpenAPI 在启动脚本里注册实例 + 定时心跳（十几行脚本的事），配置拉取同理。SDK 的连接管理、本地缓存、监听重连这些核心行为没有维护好的 Ruby 实现，自己扛就是给自己找事故。&lt;/p&gt;
&lt;h3 id=&#34;58-对象存储文件别过应用服务器&#34;&gt;5.8 对象存储：文件别过应用服务器&lt;/h3&gt;
&lt;p&gt;入门篇 §6.3 讲了 Active Storage 的用法，生产接 S3/OSS 时核心纪律只有一条：&lt;strong&gt;文件不经过 Rails 进程&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;默认链路是 浏览器 → Rails → S3：一个 100MB 上传占住一个 Puma worker 几十秒，几个并发上传就能吃光线程池（入门篇 §8.2 的并发模型在这里兑现成事故）。正确姿势是 &lt;strong&gt;Direct Upload&lt;/strong&gt;：浏览器先向 Rails 申请预签名 URL（Active Storage 自带 &lt;code&gt;/rails/active_storage/direct_uploads&lt;/code&gt; 端点），然后直传 S3，表单提交时只带一个 signed_id：&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;# 视图里一行开启&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;form&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;file_field &lt;span style=&#34;color:#e6db74&#34;&gt;:video&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;direct_upload&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&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;公开资源走 CDN 回源&lt;/strong&gt;：别让每个头像请求都打到 S3（流量费 + 延迟双输）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;私有文件用短时效签名 URL&lt;/strong&gt;：&lt;code&gt;rails_blob_url&lt;/code&gt; 的链接永久有效且经 Rails 代理；敏感文件用 &lt;code&gt;avatar.url(expires_in: 5.minutes)&lt;/code&gt; 直出 S3。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;图片变体改异步预热&lt;/strong&gt;：默认首次访问才懒生成缩略图（慢且占 worker），上传完成后用 Job 预生成（&lt;code&gt;avatar.variant(resize_to_limit: [200, 200]).processed&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;还有两个自动化问题必须回答，否则上线就是给运维埋雷：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;垃圾谁来清？不会自动发生，要两道工序。&lt;/strong&gt; 一是应用侧：浏览器直传成功但用户没提交表单，留下「传了但没关联」的孤儿 blob——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:#66d9ef&#34;&gt;ActiveStorage&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;Blob&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;unattached&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;lt; ?&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;days&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ago)&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;:purge_later&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;二是存储侧：分片上传（multipart）中断会在 S3 留下残片，&lt;strong&gt;S3 对残片照常收费&lt;/strong&gt;——bucket 上必须配 Lifecycle 规则 &lt;code&gt;AbortIncompleteMultipartUpload&lt;/code&gt;（如 7 天后中止），不配就是漏水的账单。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;传完后怎么触发后续处理？有现成挂点。&lt;/strong&gt; blob 关联到记录时，Active Storage 自动排 &lt;code&gt;AnalyzeJob&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;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Video&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;:file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  after_save_commit &lt;span style=&#34;color:#e6db74&#34;&gt;:enqueue_transcode&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; { file&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;attached? }
&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;enqueue_transcode&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;TranscodeJob&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;perform_later(id)   &lt;span style=&#34;color:#75715e&#34;&gt;# 走 §6.1 的 Active Job 链路，别在请求里做&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;只有绕过 Active Storage 直接对接桶事件的大文件管道，才需要 S3 Event Notification → SQS → 消费者的路子——那是第三篇 MQ 的地盘，一般项目用不上。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;收尾集成的本质是边界管理&#34;&gt;收尾：集成的本质是边界管理&lt;/h2&gt;
&lt;p&gt;回头看这五篇，真正难的技术点一个都没有——gem 都现成，文档都齐全。难的是&lt;strong&gt;边界上的纪律&lt;/strong&gt;：数据跨进程之后，一致性、时序、失败语义全都变了。最后三条心法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;每接一个组件，先写「它挂了怎么办」&lt;/strong&gt;。缓存挂了降级、ES 挂了回 DB、MQ 挂了走 outbox 堆积——降级路径没设计的集成，等于给系统埋雷。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据流向永远是单向的&lt;/strong&gt;。业务库 → 搜索/分析/缓存，反向只读不写。双向同步的系统没有一个不出数据事故的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;组件越少越好&lt;/strong&gt;。每接一个组件都要回答「现有组件真的解决不了吗」。SQLite 能扛的缓存别上 Redis，Sidekiq 能扛的异步别上 Kafka——&lt;strong&gt;架构的复杂度成本是持续的运维和认知负担，不是一次性接入&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;至此，从入门篇的「一个人写一个应用」，到这一篇的「让应用成为系统版图里可靠的一环」，Rails 工程师的完整拼图就齐了。&lt;/p&gt;
</description>
    </item>
    <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>
