<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>rennyallen (Renny)</title>
    <link>https://ruby-china.org/rennyallen</link>
    <description>好好学习，天天撸码…</description>
    <language>en-us</language>
    <item>
      <title>在 Rails 中使用 SSE 来实现一个 ChatGPT 应用</title>
      <description>&lt;p&gt;&lt;em&gt;原文发布于个人博客站点：&lt;a href="https://renny.ren/ch/articles/40" rel="nofollow" target="_blank"&gt;https://renny.ren/ch/articles/40&lt;/a&gt;&lt;/em&gt;  &lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="前言"&gt;前言&lt;/h2&gt;
&lt;p&gt;在使用 ChatGPT 的时候，你会注意到这个回复不是一次性生成完的，而是边生成边返回，像打字一样的效果：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/rennyallen/1e3d71cc-8738-4ccc-958e-cce089b482db.gif!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;那么这是如何实现的呢，这篇来研究一下相关的技术细节。&lt;/p&gt;

&lt;p&gt;其实这种效果叫 streaming response (流式传输的回复)，很形象。&lt;/p&gt;

&lt;p&gt;提到 streaming response 就不得不提到 SSE&lt;/p&gt;
&lt;h2 id="关于 SSE"&gt;关于 SSE&lt;/h2&gt;
&lt;p&gt;如果你看一下 &lt;a href="https://platform.openai.com/docs/api-reference/chat/create" rel="nofollow" target="_blank" title=""&gt;OenAI API 文档&lt;/a&gt;，就会发现有一个参数叫 &lt;code&gt;stream&lt;/code&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If set, partial message deltas will be sent, like in ChatGPT. Tokens will be sent as data-only &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format" rel="nofollow" target="_blank" title=""&gt;server-sent events&lt;/a&gt; as they become available, with the stream terminated by a &lt;code&gt;data: [DONE]&lt;/code&gt; message.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;所以什么是 SSE 呢？&lt;/p&gt;

&lt;p&gt;简单来说，SSE (Server-Sent Event) 是一种从服务器流式传输事件的简单方式。它通过单个 HTTP 连接将实时更新从服务器发送到客户端。使用 SSE，只要建立连接后，服务器就可以将实时数据推送到客户端，无需靠客户端不断轮询来获取更新。&lt;/p&gt;

&lt;p&gt;步骤如下：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;客户端发送 GET 请求到服务器：&lt;code&gt;https://www.host.com/stream&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;建立长连接，响应头里会有 &lt;code&gt;Connection: keep-alive&lt;/code&gt; (从 HTTP/1.1 起，默认就使用的是长连接)&lt;/li&gt;
&lt;li&gt;服务端设置 &lt;code&gt;Content-Type: text/event-stream&lt;/code&gt; response header&lt;/li&gt;
&lt;li&gt;服务端可以开始发送事件 (event) 了，类似这样：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;event: add
data: This is the first message, it
data: has two lines.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就是这么简单&lt;/p&gt;
&lt;h2 id="比较一下 SSE 和 WebSocket"&gt;比较一下 SSE 和 WebSocket&lt;/h2&gt;
&lt;p&gt;那么  SSE 和 WebSocket 是不是差不多呢？总结了一下，它们都是可以用来在客户端和服务端做实时通信的，但有一些小的区别：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;SSE 提供的是单向通信渠道 (server -&amp;gt; client)；而 WebSockets 是双向沟通，客户端也可以随时给服务器发消息&lt;/li&gt;
&lt;li&gt;SSE 是基于 HTTP 的，本质上还是使用长轮询技术来实现实时通信；而 WebSocket 则直接在 TCP 连接上发送和接收数据&lt;/li&gt;
&lt;li&gt;SSE 在连接丢失的时候会自动尝试重连，重连失败又会重连，无限重连。。所以你在浏览器看到的就是 GET 请求无限发送，一直到服务器返回连接成功为止，所以需要加上处理异常的代码，在 client 端关闭连接；而 WebSocket 如果连接丢失了一般是需要 client 重新建立一个新的连接&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;总的来说，SSE 使用简单，更适合传输小量的数据，特别是只需要服务端到客户端单向通信的时候。WebSocket 要更强大一些，可以用于更多的复杂场景，比如多人实时聊天、多人游戏等。&lt;/p&gt;
&lt;h2 id="Workflow"&gt;Workflow&lt;/h2&gt;
&lt;p&gt;接下来以 Rails 提供后端接口为例，看看怎么调用 OpenAI 的接口 接收 SSE 事件，然后转发到我们的客户端。&lt;/p&gt;

&lt;p&gt;工作流程是这样的：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/rennyallen/98b75934-db2c-4f48-90ae-062921a4d356.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;客户端使用 &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/EventSource" rel="nofollow" target="_blank" title=""&gt;EventSource&lt;/a&gt; 接口向服务端发送请求&lt;/li&gt;
&lt;li&gt;服务端收到请求，发送请求到 OpenAI 接口，带上 &lt;code&gt;stream: true&lt;/code&gt; 参数&lt;/li&gt;
&lt;li&gt;服务端收到来自 OpenAI 的 event，然后转发给客户端&lt;/li&gt;
&lt;li&gt;当事件发送完毕后，OpenAI 会发送一个特殊的消息来告诉我们可以关闭连接了。比如当我们收到 &lt;code&gt;[Done]&lt;/code&gt;，就可以关闭服务器与 OpenAI 之间的连接，然后客户端关闭到我们服务器的连接&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="用 Rails 提供后端接口"&gt;用 Rails 提供后端接口&lt;/h2&gt;
&lt;p&gt;理解了 SSE 和工作流之后，接下来就是代码来实现整个过程。总共三个部分：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;client&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;evtSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/v1/completions/live_stream?prompt=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;evtSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nf"&gt;setMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;evtSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;evtSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;evtSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用上面提到的 &lt;code&gt;EventSource&lt;/code&gt; API 来建立 SSE 链接。
当收到新消息的时候，&lt;code&gt;onmessage&lt;/code&gt; 事件会被触发 &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;server&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CompletionsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;ActionController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Live&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;live_stream&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"text/event-stream"&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Last-Modified"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;httpdate&lt;/span&gt;
    &lt;span class="n"&gt;sse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SSE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;retry: &lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;ChatCompletion&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;LiveStreamService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;live_stream_params&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;
  &lt;span class="k"&gt;ensure&lt;/span&gt;
    &lt;span class="n"&gt;sse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这里引入了 &lt;code&gt;ActionController::Live&lt;/code&gt; module 来开启 streaming response&lt;/li&gt;
&lt;li&gt;上面提到了，需要设置 &lt;code&gt;text/event-stream&lt;/code&gt; response header&lt;/li&gt;
&lt;li&gt;这里需要特别注意的是，如果你使用的是 Rails 7, Rails 7 默认是不支持 stream repsonse 的，这个问题我找了好久，最后发现是 rack 的问题&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rails 7 默认是引入了 &lt;code&gt;Rack::ETag&lt;/code&gt; 的，而这玩意会把 response 缓存起来，导致实时的 streaming response 就不能实现了。&lt;/p&gt;

&lt;p&gt;这个问题我看 issue 里面讨论了好久，到最后也没有解决，不过有 hack 的解决方案，具体可以参考&lt;a href="https://github.com/rack/rack/issues/1619#issuecomment-1510031078" rel="nofollow" target="_blank" title=""&gt;这里&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;总之如果你的 rack 版本是 &lt;code&gt;2.2.x&lt;/code&gt; 就需要加下面这一行：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Last-Modified"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;httpdate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;OpenAI API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;接下来是请求 OpenAI 接口的部分，自己封装了&lt;a href="https://github.com/renny-ren/openai_ruby" rel="nofollow" target="_blank" title=""&gt;一个简单的 gem&lt;/a&gt;，支持流式传输&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ChatCompletion&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LiveStreamService&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
      &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_chat_completion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request_body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;overall_received_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;/data: (.*)\n\n$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"choices"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"delta"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="vi"&gt;@result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@result&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"choices"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"delta"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="n"&gt;sse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="vi"&gt;@result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="kp"&gt;private&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;client&lt;/span&gt;
      &lt;span class="vi"&gt;@client&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="no"&gt;OpenAI&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，我上面写的是前后端分离的方案，如果你在用 Hotwire 的话，可以看看&lt;a href="https://gist.github.com/alexrudall/cb5ee1e109353ef358adb4e66631799d" rel="nofollow" target="_blank" title=""&gt;这篇&lt;/a&gt; 和 &lt;a href="https://ruby-china.org/topics/42959" rel="nofollow" target="_blank"&gt;https://ruby-china.org/topics/42959&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;最后，我做了一个 demo，大家可以体验效果：&lt;a href="https://aiichat.cn/chats/new" rel="nofollow" target="_blank"&gt;https://aiichat.cn/chats/new&lt;/a&gt; (登录账号密码皆为 rubychina)&lt;/p&gt;</description>
      <author>rennyallen</author>
      <pubDate>Fri, 05 May 2023 20:09:21 +0800</pubDate>
      <link>https://ruby-china.org/topics/43052</link>
      <guid>https://ruby-china.org/topics/43052</guid>
    </item>
    <item>
      <title>Migrate from Webpacker to Vite 从入门到放弃</title>
      <description>&lt;h3 id="前言"&gt;前言&lt;/h3&gt;
&lt;p&gt;听闻 &lt;a href="https://vitejs.dev/" rel="nofollow" target="_blank" title=""&gt;Vite&lt;/a&gt; 速度很快，最近开始考虑把项目里面的 webpack 打包换成 Vite&lt;/p&gt;

&lt;p&gt;虽然折腾了一波之后最后放弃了，还是记录一下过程，算是一篇踩坑记录&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;由于是 Rails 项目，我就直接使用封装好的 &lt;a href="https://github.com/ElMassimo/vite_ruby" rel="nofollow" target="_blank" title=""&gt;vite_ruby&lt;/a&gt; 开始搞了&lt;/p&gt;

&lt;p&gt;看了下官方文档，感觉很清晰，再根据 &lt;a href="https://github.com/ElMassimo/pingcrm-vite/pull/1" rel="nofollow" target="_blank" title=""&gt;这个步骤&lt;/a&gt;，常规操作搞一波，该 install 的 install 完，再替换下 javascript_packs_tag 什么的，试了一下启动，果然飞快，感觉要大功告成了：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/rennyallen/8a6bdc8a-8884-4fc4-9c8c-8b1ca99bf03e.png!large" width="300px" alt=""&gt;&lt;/p&gt;

&lt;hr&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;再一看发现样式都没有出来，css 文件没有加载，得用  &lt;code&gt;&amp;lt;%= vite_stylesheet_tag 'application.scss' %&amp;gt;&lt;/code&gt; 加载一下，另外如果是 scss 格式的话必须显示指定一下&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;接下来会发现大量这样的错误：&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/rennyallen/b3211d78-9c85-4912-a8c6-3c0d060e8572.png!large" width="500px" alt=""&gt;&lt;/p&gt;

&lt;p&gt;原因很简单，我们项目里用了 React 自然有大量的 JSX 语法，但是我们很多文件后缀是 .js 命名的，而在 JS 文件里的 JSX 语法是被 Vite 认为不支持的&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Vite assumes that js files contain only js valid syntax. But jsx has another AST and therefore is only supported in jsx files&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;尤大大也说了 (&lt;a href="https://twitter.com/youyuxi/status/1362050255009816577" rel="nofollow" target="_blank"&gt;https://twitter.com/youyuxi/status/1362050255009816577&lt;/a&gt;)，本来 JS 文件大多数情况下是不需要完全的 AST transform 的，如果要支持 JS 文件里的 JSX 语法的话，相当于所有文件都需要被 full-AST-processed.  这样其实不是一个好的做法&lt;/p&gt;

&lt;p&gt;那怎么解决呢？找了三个方案：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;替换所有 .js 文件为 .jsx 或者 .tsx&lt;/li&gt;
&lt;li&gt;使用 plugin (&lt;a href="https://github.com/dravenww/vite-plugin-react-js-support" rel="nofollow" target="_blank"&gt;https://github.com/dravenww/vite-plugin-react-js-support&lt;/a&gt;) 增加对 JS 文件中的语法支持&lt;/li&gt;
&lt;li&gt;使用 esbuilder 配置 loader 来解析文件，原理和方案 2 一样&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;最后我选了方案 1，虽然要替换几百个文件看起来改动很大，但这个应该才是正确的做法，本来就不该命名为 .js&lt;/p&gt;

&lt;p&gt;跑了个 shell 批量给替换了：&lt;code&gt;find app/javascript/components -name "*.js" -exec sh -c 'mv "$0" "${0%.js}.tsx"' {} \;&lt;/code&gt;&lt;/p&gt;

&lt;hr&gt;

&lt;ul&gt;
&lt;li&gt;JSX 的问题搞定了，发现出现了另一个问题，大量的文件在 import 的时候找不到，之前用相对路径写的全都不能用了&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;例如 &lt;code&gt;import XX from 'components/form/xxxx'&lt;/code&gt; 都得替换成 &lt;code&gt;import XX from '~/components/form/xxxx'&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;这个好解决，在  vite.config.ts 里配置一下 resolve path 就好了：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;RubyPlugin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;projectRoot&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vite-plugin-ruby&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;RubyPlugin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;find&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;components&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;replacement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app/javascript/components&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;find&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shared&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;replacement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app/javascript/shared&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;find&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;actions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;replacement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app/javascript/actions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;find&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stores&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;replacement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app/javascript/stores&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;

&lt;ul&gt;
&lt;li&gt;下一个问题，Webpack 里的 require.context 在 Vite 里是不能用的，需要用 &lt;a href="https://vitejs.dev/guide/features.html#glob-import" rel="nofollow" target="_blank" title=""&gt;glob-import&lt;/a&gt; 来做&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;比如 &lt;code&gt;const componentRequireContext = require.context('components', true)&lt;/code&gt; 需要改成 &lt;code&gt;const componentRequireContext = import.meta.globEager('../components/**')&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;另外其他用到 require 的地方也得改成 import&lt;/p&gt;

&lt;p&gt;否则就会出现 &lt;a href="https://github.com/vitejs/vite/issues/2161" rel="nofollow" target="_blank" title=""&gt;require is not defined&lt;/a&gt; 的报错&lt;/p&gt;

&lt;p&gt;因为 Vite 是完全依靠 ESM 原生能力的，也就是说它只认 import, 而 require 是 CJS 里的方法。我们的代码最终被送到浏览器里执行，浏览器本身就没定义这个方法，所以就报错了。&lt;/p&gt;

&lt;p&gt;这里和 Webpack 不一样，Webpack 事先会编译打包好，到浏览器的时候已经把 require 转成浏览器能识别的方式了。&lt;/p&gt;

&lt;p&gt;所以这里还是有点难受的，好在我这项目里 require 的地方不多，勉强把这个坑踩过去了。&lt;/p&gt;

&lt;hr&gt;

&lt;ul&gt;
&lt;li&gt;最后终于把所有报错解决了，也跑起来了，发现有点不对，页面显示基本都不完整，很多东西都没显示出来。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这里要说的是，我们项目里很多地方不是 React 全局接管路由的，也就是说不是纯前端渲染，而是用了 &lt;a href="https://github.com/reactjs/react-rails" rel="nofollow" target="_blank" title=""&gt;react_rails&lt;/a&gt; 的方式来渲染的前端组件，问题就出在这儿，这种方式渲染的内容都没有显示出来。&lt;/p&gt;

&lt;p&gt;猜测是这种方式引用的前端组件没有被 Vite 识别到。&lt;/p&gt;

&lt;p&gt;于是翻到了社区的讨论：&lt;/p&gt;

&lt;p&gt;vite_ruby 这边作者表示自己没有用过 react_rails, 可能不太兼容，建议 react_rails 作者去支持组件注册 &lt;a href="https://github.com/ElMassimo/vite_ruby/discussions/86" rel="nofollow" target="_blank"&gt;https://github.com/ElMassimo/vite_ruby/discussions/86&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;而看到 react_rails 这边对于 Vite 的支持也没有明确进展 &lt;a href="https://github.com/reactjs/react-rails/issues/1134" rel="nofollow" target="_blank"&gt;https://github.com/reactjs/react-rails/issues/1134&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;玩到这里，我就基本放弃了，溜了溜了。&lt;/p&gt;

&lt;hr&gt;
&lt;h3 id="结语"&gt;结语&lt;/h3&gt;
&lt;p&gt;如果是对于新项目，确实可以尝试 Vite，速度会有提升；如果对于依赖太多的老项目，迁移起来可能会比较麻烦。&lt;/p&gt;</description>
      <author>rennyallen</author>
      <pubDate>Sat, 17 Jul 2021 11:28:30 +0800</pubDate>
      <link>https://ruby-china.org/topics/41482</link>
      <guid>https://ruby-china.org/topics/41482</guid>
    </item>
    <item>
      <title>[译] 减慢 Rails 应用的 3 个 ActiveRecord 错用</title>
      <description>&lt;p&gt;&lt;em&gt;原文：&lt;a href="https://www.speedshop.co/2019/01/10/three-activerecord-mistakes.html" rel="nofollow" target="_blank"&gt;https://www.speedshop.co/2019/01/10/three-activerecord-mistakes.html&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
&lt;em&gt;作者：&lt;a href="https://twitter.com/nateberkopec" rel="nofollow" target="_blank" title=""&gt;Nate Berkopec&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
&lt;em&gt;译者注：本文采用意译，部分语句为保留原意未作翻译，推荐有条件者查看英文原文。水平有限，请多指教。&lt;/em&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h5 id="3 ActiveRecord Mistakes That Slow Down Rails Apps: Count, Where and Present"&gt;3 ActiveRecord Mistakes That Slow Down Rails Apps: Count, Where and Present&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;许多 Rails 开发者不了解导致 ActiveRecord 执行一条 SQL 查询的真正原因是什么。我们来看看三个常见情况：滥用 count 方法、用 where 来 查询子集，以及 &lt;code&gt;present?&lt;/code&gt; 校验。如果你滥用这三个方法，可能会导致额外的 N+1 查询。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;ActiveRecord 确实很好，但它是一个已经封装好的抽象化的东西，容易让你忽略掉真实跑在数据库里的 SQL 查询。如果你不了解 ActiveRecord 是怎么工作的，你可能会搞出一些你其实不想要的 SQL 查询。 &lt;/p&gt;

&lt;p&gt;不幸的是，ActiveRecord 的许多功能的性能消耗意味着我们不能忽略那些不必要的开销，或是仅仅把我们的 ORM 看做一个实现细节。我们需要了解到底哪些查询影响了性能表现。世上没有真正的自由，ActiveRecord 亦如此。&lt;/p&gt;

&lt;p&gt;我发现一个典型的情况是，ActiveRecord 在执行一些并不是真正需要的 SQL，然而大多数人完全不知道这个事情。&lt;/p&gt;

&lt;p&gt;不必要的 SQL 是造成 controller action 十分缓慢的一个主要原因，特别是当不必要的查询出现在一个 partial 里，而这个 partial 又被一个 collection 里的每个 element 遍历渲染。这在 search action 或者 index action 中时常发生。这是我在面对性能咨询时候最常见的问题之一，它几乎是我遇到过的每个应用都会有的一个问题。&lt;/p&gt;

&lt;p&gt;消除不必要的查询的一个方法是，钻研并理解 ActiveRecord 的内部实现细节，理解某一个方法究竟是怎么实现的。
&lt;strong&gt;今天，就让我们来看看在 Rails 应用中造成许多不必要查询的三个方法的实现和用法，这三个方法分别是：&lt;code&gt;count&lt;/code&gt;, &lt;code&gt;where&lt;/code&gt;, &lt;code&gt;present?&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2019/7f872837-ce45-4fa7-a240-159916a03300.png!large" title="" alt=""&gt;&lt;/p&gt;
&lt;h6 id="　"&gt; &lt;/h6&gt;&lt;h2 id="我咋知道一个查询是不必要的呢？"&gt;我咋知道一个查询是不必要的呢？&lt;/h2&gt;
&lt;p&gt;我有一个判断某个 SQL 查询是否必要的经验法则。理想情况下，在一个 controller 的 action 中，&lt;strong&gt;对于一张表应该只执行一条 SQL 查询&lt;/strong&gt;。如果你看到某张表超过了一条 SQL 查询，通常你可以找到方法来减少到一条或两条查询。如果你发现针对某张表有许多 SQL 查询，几乎可以说你肯定能找到不必要的查询。(杠精就不要来了，这是一条 guideline，不是 rule, 我知道有的情况下一张表超过一条查询是没问题的)&lt;/p&gt;

&lt;p&gt;如果你安装了 NewRelic 的话，某张表的 SQL 查询数量很容易查阅。
&lt;img src="https://www.speedshop.co/assets/posts/img/nplusoneposts-7a096c5fa29cd74436d17d40baf35963b98bb92a47f9af7ad56c650ed8dfce68.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;另外一条法则是，大多数查询的执行都应该是&lt;strong&gt;在一个 action 的 response 的前半段 (during the first half of a controller action’s response)，基本上不会在 partial 里面&lt;/strong&gt;。在 partial 里面执行的查询通常都不是有意的，而且经常都是 N+1。如果你注意在开发环境下看日志的话，这些在一个 controller 执行的时候很容易发现。比如，如果你看到长这样的：&lt;/p&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;User&lt;/span&gt; &lt;span class="k"&gt;Load&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;SELECT&lt;/span&gt;  &lt;span class="nv"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"users"&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nv"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"id"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="nv"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="n"&gt;Rendered&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;_post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;erb&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;User&lt;/span&gt; &lt;span class="k"&gt;Load&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;SELECT&lt;/span&gt;  &lt;span class="nv"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"users"&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nv"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"id"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="nv"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="n"&gt;Rendered&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;_post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;erb&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;... 看到了吧，这个局部视图里面 N+1 了。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2019/004de78a-4265-43df-b82b-14f5df51f4e3.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;我旁边桌子上有个洗眼装置，专治 N+1&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;通常，如果一个查询在某个 controller action 的中途执行（比如一个 partial 中某个较深的地方），这意味着你没有  &lt;a href="https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-preload" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;preload&lt;/code&gt;&lt;/a&gt; 你需要的数据。&lt;/p&gt;

&lt;p&gt;所以，让我们专门看看 &lt;code&gt;count&lt;/code&gt;, &lt;code&gt;where&lt;/code&gt; 和 &lt;code&gt;present?&lt;/code&gt; 三个方法，为什么他们会造成不必要的 SQL 查询&lt;/p&gt;
&lt;h6 id="　"&gt; &lt;/h6&gt;&lt;h2 id=".count executes a COUNT every time"&gt;.count executes a COUNT every time&lt;/h2&gt;
&lt;p&gt;几乎我工作过的每家公司都有这个问题。似乎并不是很多人知道，在一个 ActiveRecord relation 上调用 &lt;code&gt;count&lt;/code&gt; 方法总是会执行一次 SQL 查询，每次都是。这在大多数场景下并不合适，通常来讲，&lt;strong&gt;只有在你希望立即执行一次 SQL COUNT 查询的时候才需要用 &lt;code&gt;count&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;最常见的导致不必要的 &lt;code&gt;count&lt;/code&gt; 查询的原因是，你对一个之后会在 view 中用到的 association 调用 &lt;code&gt;count&lt;/code&gt; ：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# _messages.html.erb
# Assume @messages = user.messages.unread, or something like that

&amp;lt;h2&amp;gt;Unread Messages: &amp;lt;%= @messages.count %&amp;gt;&amp;lt;/h2&amp;gt;

&amp;lt;% @messages.each do |message| %&amp;gt;
blah blah blah
&amp;lt;% end %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就执行了两次查询，a &lt;code&gt;COUNT&lt;/code&gt; and a &lt;code&gt;SELECT&lt;/code&gt;. The COUNT is executed by &lt;code&gt;@messages.count&lt;/code&gt;, and &lt;code&gt;@messages.each&lt;/code&gt; executes a SELECT to load all the messages. 
改变代码在 partial 中的顺序，把 &lt;code&gt;count&lt;/code&gt; 改成 &lt;code&gt;size&lt;/code&gt; 来完全消除 &lt;code&gt;COUNT&lt;/code&gt; 查询，&lt;code&gt;SELECT&lt;/code&gt; 还是保留：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;% @messages.each do |message| %&amp;gt;
blah blah blah
&amp;lt;% end %&amp;gt;

&amp;lt;h2&amp;gt;Unread Messages: &amp;lt;%= @messages.size %&amp;gt;&amp;lt;/h2&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么呢？我们来看看&lt;a href="https://github.com/rails/rails/blob/94b5cd3a20edadd6f6b8cf0bdf1a4d4919df86cb/activerecord/lib/active_record/relation.rb#L210" rel="nofollow" target="_blank" title=""&gt;ActiveRecord::Relation 中 size 的实现&lt;/a&gt;就行了&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# File activerecord/lib/active_record/relation.rb, line 210&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;size&lt;/span&gt;
  &lt;span class="n"&gt;loaded?&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="vi"&gt;@records.length&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:all&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 relation 已经加载（也就是说，relation 对应的查询已经执行并且我们已经存储好了结果），我们可以在已经加载好的记录数组上调用 &lt;code&gt;length&lt;/code&gt; 方法。&lt;a href="https://ruby-doc.org/core-2.5.0/Array.html#method-i-length" rel="nofollow" target="_blank" title=""&gt;这只是 ruby array 的一个简单方法&lt;/a&gt;。如果 ActiveRecord::Relation 还没有加载好，就会触发一次 &lt;code&gt;COUNT&lt;/code&gt; 查询。&lt;/p&gt;

&lt;p&gt;另一方面，这是 &lt;a href="https://github.com/rails/rails/blob/94b5cd3a20edadd6f6b8cf0bdf1a4d4919df86cb/activerecord/lib/active_record/relation/calculations.rb#L41" rel="nofollow" target="_blank" title=""&gt;count 的实现&lt;/a&gt; (in ActiveRecord::Calculations):&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;column_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;block_given?&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;column_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，&lt;a href="https://github.com/rails/rails/blob/94b5cd3a20edadd6f6b8cf0bdf1a4d4919df86cb/activerecord/lib/active_record/relation/calculations.rb#L131" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;calculate&lt;/code&gt;的实现&lt;/a&gt;并不会缓存任何东西，所以每次调用都会执行一次 SQL&lt;/p&gt;

&lt;p&gt;在我们上面的实例中，把 &lt;code&gt;count&lt;/code&gt; 改成 &lt;code&gt;size&lt;/code&gt; 仍然会触发 &lt;code&gt;COUNT&lt;/code&gt; 查询。数据没有被加载吗？当 &lt;code&gt;size&lt;/code&gt;被调用的时候，ActiveRecord 仍然会尝试一次 &lt;code&gt;COUNT&lt;/code&gt;。我们把方法移到数据加载之后就消除了这条查询。现在，把 header 移动到 partial 的尾部在逻辑上并不太 make sense。我们可以用 &lt;code&gt;load&lt;/code&gt; 方法。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;h2&amp;gt;Unread Messages: &amp;lt;%= @messages.load.size %&amp;gt;&amp;lt;/h2&amp;gt;

&amp;lt;% @messages.each do |message| %&amp;gt;
blah blah blah
&amp;lt;% end %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;load&lt;/code&gt; 会立即加载 &lt;code&gt;@messages&lt;/code&gt; 的所有记录，而不是懒加载。 &lt;a href="https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-load" rel="nofollow" target="_blank" title=""&gt;它会返回 ActiveRecord::Relation，而不是 records&lt;/a&gt;。所以，当 &lt;code&gt;size&lt;/code&gt; 方法被调用，记录都是 &lt;code&gt;loaded?&lt;/code&gt; 并且已经避免了查询。&lt;img title=":ox:" alt="🐂" src="https://twemoji.ruby-china.com/2/svg/1f402.svg" class="twemoji"&gt; &lt;img title=":beer:" alt="🍺" src="https://twemoji.ruby-china.com/2/svg/1f37a.svg" class="twemoji"&gt; &lt;/p&gt;

&lt;p&gt;那如果，我们用 &lt;code&gt;messages.load.count&lt;/code&gt; 呢？仍然会触发一次 COUNT 查询！&lt;/p&gt;

&lt;p&gt;什么时候 &lt;code&gt;count&lt;/code&gt; 才不会触发查询呢？只有当这个结果已经被 &lt;code&gt;ActiveRecord::QueryCache&lt;/code&gt; 缓存好了的时候。比如这样相同的命令执行两次的时候：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&amp;lt;h2&amp;gt;Unread Messages: &amp;lt;%= @messages.count %&amp;gt;&amp;lt;/h2&amp;gt;

... lots of other view code, then later:

&amp;lt;h2&amp;gt;Unread Messages: &amp;lt;%= @messages.count %&amp;gt;&amp;lt;/h2&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;我认为大多数 Rails 开发者在他们用 &lt;code&gt;count&lt;/code&gt; 的大多数地方，都应该用 &lt;code&gt;size&lt;/code&gt;。&lt;/strong&gt; 我不知道为什么好像大家都喜欢用 &lt;code&gt;count&lt;/code&gt; 而不是 &lt;code&gt;size&lt;/code&gt;。 &lt;code&gt;size&lt;/code&gt; 会在合适的时候使用 &lt;code&gt;count&lt;/code&gt;，在记录已经加载的时候不做多余的 COUNT。我觉得这是因为当你在操作 ActiveRecord relation 的时候，你还处在 "SQL" 的思维里。你想着：“这是 SQL 语句，我应该写 &lt;code&gt;count&lt;/code&gt; 来执行 COUNT 操作！”&lt;/p&gt;

&lt;p&gt;所以，你到底什么时候需要用 &lt;code&gt;count&lt;/code&gt; 呢？Use it when you won’t actually ever be loading the full association that you’re counting. 比如，来看看 Rubygems.org 上的一个页面，这里展示了一个 gem:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.speedshop.co/assets/posts/img/rspecview-9229ad8cdcaaf7aad42c0403811ce5eee825eb3fd49fc4536ce992b4fff7c6cd.png" rel="nofollow" target="_blank" title=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;在 "verisions" 列表中，这里调用了 &lt;code&gt;count&lt;/code&gt; 来获取这个 gem 的 release version 总数。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/rubygems/rubygems.org/blob/d8a48488d29cbfc83efd2e936c74290c54041288/app/views/rubygems/show.html.erb#L36" rel="nofollow" target="_blank" title=""&gt;这是源码&lt;/a&gt;：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;% if show_all_versions_link?(@rubygem) %&amp;gt;
  &amp;lt;%= link_to t('.show_all_versions', :count =&amp;gt; @rubygem.versions.count), rubygem_versions_url(@rubygem), :class =&amp;gt; "gem__see-all-versions t-link--gray t-link--has-arrow" %&amp;gt;
&amp;lt;% end %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键是，这个页面上 &lt;u&gt;永远不会&lt;/u&gt; 加载这个 gem 的&lt;u&gt;所有&lt;/u&gt;版本。这个版本列表里只会加载最近的五个版本。&lt;/p&gt;

&lt;p&gt;所以，&lt;code&gt;count&lt;/code&gt; 方法在这里就完美适用，虽然 &lt;code&gt;size&lt;/code&gt; 在逻辑上是相等的（也会执行一次 COUNT，因为 &lt;code&gt;@verisions.loaded?&lt;/code&gt; 为 false）&lt;/p&gt;

&lt;p&gt;我的建议是，在你的 &lt;code&gt;app/views&lt;/code&gt; 目录下搜索 &lt;code&gt;count&lt;/code&gt; 调用，确保他们真的 make sense. 如果你不是 100% 确定你真的需要在那个地方立即做 SQL COUNT 查询，那就把它改成 &lt;code&gt;size&lt;/code&gt;。最坏的情况下，如果关联关系没有加载，ActiveRecord 仍然会执行一次 COUNT。如果你之后在 view 中会用到这个关联关系，把它改成 &lt;code&gt;load.size&lt;/code&gt;&lt;/p&gt;
&lt;h6 id="　"&gt; &lt;/h6&gt;&lt;h2 id=".where means filtering is done by the database"&gt;.where means filtering is done by the database&lt;/h2&gt;
&lt;p&gt;来看看这代码有什么问题：(&lt;code&gt;_post.html.erb&lt;/code&gt;)&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;% @posts.each do |post| %&amp;gt;
  &amp;lt;%= post.content %&amp;gt;
  &amp;lt;%= render partial: :comment, collection: post.active_comments %&amp;gt;
&amp;lt;% end %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and in &lt;code&gt;post.rb&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;active_comments&lt;/span&gt;
    &lt;span class="n"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;soft_deleted: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你说，“this causes a SQL query to be executed on every rendering of the post partial”，回答正确！&lt;code&gt;where&lt;/code&gt; 总是会发生查询。我没把 controller 层的代码写出来，因为这没有影响。你不能用诸如 &lt;code&gt;includes&lt;/code&gt; 的预加载方法来 stop 这个查询，&lt;code&gt;where&lt;/code&gt; 总是会执行查询的！&lt;/p&gt;

&lt;p&gt;当你在关联关系上调 scope 时也是一样的。想象我们有一个这样的  comment model:&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Comment&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:post&lt;/span&gt;

  &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="ss"&gt;:active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;soft_deleted: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;允许我总结两条规则： &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;当你在 render collections 的时候，不要在关联关系上调用 scope； &lt;/li&gt;
&lt;li&gt;不要在一个 ActiveRecord::Base 类里面的实例方法里写查询方法，比如 &lt;code&gt;where&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;在关联关系上调用 scope 意味着我们不能预加载结果。在上面的例子中，我们可以预加载一个 post 中的 comments，但是我们不能预加载一个 post 的 active comments, 所以我们对于 collection 中的 每个 element 都必须到数据库中去执行新的查询。&lt;/p&gt;

&lt;p&gt;如果你只做一次这个操作还没什么问题，但是如果对于一个 collection 的 每个 element 都这样就不行了（比如上面的，每一篇 post)
在这样的情况下可以用 scope - 比如，一个 PostsController#show action 中，只显示一篇 post 以及和它相关联的 comments.
但如果是 collections, scopes on associations 总会造成 N + 1.&lt;/p&gt;

&lt;p&gt;我找到了解决这个问题的最佳方法是&lt;strong&gt;建一个新的 association&lt;/strong&gt;。&lt;a href="https://www.justinweiss.com/" rel="nofollow" target="_blank" title=""&gt;Justin Weiss&lt;/a&gt; 的 "Practicing Rails" 中的 &lt;a href="https://www.justinweiss.com/articles/how-to-preload-rails-scopes/" rel="nofollow" target="_blank" title=""&gt;这篇文章&lt;/a&gt; 教会了我预加载 rails scopes 相关的东西。Idea 就是去建一个可以 preload 的新的 association:&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:comments&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:active_comments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;class_name: &lt;/span&gt;&lt;span class="s2"&gt;"Comment"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Comment&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:post&lt;/span&gt;
  &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="ss"&gt;:active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;soft_deleted: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostsController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
    &lt;span class="vi"&gt;@posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:active_comments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;view 层没有变化，但是现在只用执行两条 SQL 了，一条在 Posts 表上，一条在 Comments 表上。Nice!&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;% @posts.each do |post| %&amp;gt;
  &amp;lt;%= post.content %&amp;gt;
  &amp;lt;%= render partial: :comment, collection: post.active_comments %&amp;gt;
&amp;lt;% end %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二条经验是，&lt;strong&gt;不要把查询方法（比如 &lt;code&gt;where&lt;/code&gt;) 放到 ActiveRecord::Base 类里的某个实例方法中&lt;/strong&gt;。这个可能不太明显，来看个示例：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:post&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;latest_comment&lt;/span&gt;
    &lt;span class="n"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'published_at desc'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 view 层长这样，会发生什么呢？&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;% @posts.each do |post| %&amp;gt;
  &amp;lt;%= post.content %&amp;gt;
  &amp;lt;%= render post.latest_comment %&amp;gt;
&amp;lt;% end %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不管你预加载了什么，对于每一篇 post 都有 SQL 查询。以我的经验，&lt;strong&gt;每一个 ActiveRecord::Base 类里的实例方法，最终都会在一个 collection 中被调用&lt;/strong&gt;。一个方法，可能最开始是路人甲写的，然后路人乙加了个新的 feature 但是并没注意，没有完全理解这个方法的实现，然后，N+1 出现了。我刚给的这个示例可以用前面说的 association 来重写，虽然仍然会有 N+1，但至少可以通过正确的预加载轻松解决。&lt;/p&gt;

&lt;p&gt;那么哪些 ActiveRecord 方法是我们在 model 的实例方法中应该避免的呢？一般来说，基本包括所有的 &lt;a href="https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html" rel="nofollow" target="_blank" title=""&gt;QueryMethods&lt;/a&gt;, &lt;a href="https://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html" rel="nofollow" target="_blank" title=""&gt;FinderMethods&lt;/a&gt; 和 &lt;a href="https://api.rubyonrails.org/classes/ActiveRecord/Calculations.html" rel="nofollow" target="_blank" title=""&gt;Calculations&lt;/a&gt;. 这些方法通常都会执行 SQL 查询，并且预加载还不一定治得了。&lt;/p&gt;
&lt;h6 id="　"&gt; &lt;/h6&gt;&lt;h2 id="any?, exists? and present?"&gt;any?, exists? and present?&lt;/h2&gt;
&lt;p&gt;Rails 开发者一直被一个烦恼困扰着 -- 那就是他们要给各个变量加特定的断言方法。&lt;code&gt;present?&lt;/code&gt; 已经散播到了 Rails 项目的各个角落，比 13 世纪欧洲的瘟疫还快。绝大部分时候，断言方法没有增加什么，很冗余，其实我们想要的只是一个 true or false 的判断，只用写个变量名就能搞定的那种。&lt;/p&gt;

&lt;p&gt;这是来自 &lt;a href="https://www.codetriage.com/" rel="nofollow" target="_blank" title=""&gt;CodeTriage&lt;/a&gt; 的一个 &lt;a href="https://github.com/codetriage/codetriage/blob/b92e347e0f4714b4646be930e341be5a44761b95/app/models/doc_comment.rb#L9" rel="nofollow" target="_blank" title=""&gt;示例&lt;/a&gt;，这是我朋友&lt;a href="https://schneems.com/" rel="nofollow" target="_blank" title=""&gt;Richard Schneeman&lt;/a&gt; 写的一个免费的 Rails 开源项目：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DocComment&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:doc_method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;counter_cache: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;

  &lt;span class="c1"&gt;# ... things removed for clarity...&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;doc_method?&lt;/span&gt;
    &lt;span class="n"&gt;doc_method_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;present?&lt;/code&gt; 在这里做什么？第一，把 &lt;code&gt;doc_method_id&lt;/code&gt; 从 &lt;code&gt;nil&lt;/code&gt; 或者一个 Integer 转成 &lt;code&gt;true&lt;/code&gt; 或者 &lt;code&gt;false&lt;/code&gt;. 对于断言方法必须要返回 &lt;code&gt;true&lt;/code&gt; 或者 &lt;code&gt;false&lt;/code&gt; 这两个值中的一个，还是说只要返回一个逻辑上为真或假的值就行，有些人有自己鲜明的观点，我没有。但是加 &lt;code&gt;present?&lt;/code&gt; 同时也带了其他东西进来，让我们来看看&lt;a href="https://github.com/rails/rails/blob/94b5cd3a20edadd6f6b8cf0bdf1a4d4919df86cb/activesupport/lib/active_support/core_ext/object/blank.rb#L26" rel="nofollow" target="_blank" title=""&gt;实现&lt;/a&gt;：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;present?&lt;/span&gt;
    &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;blank?&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;blank?&lt;/code&gt; 是一个比“这个对象是真还是假”还要复杂的问题。空数组和哈希是真，但是是 &lt;code&gt;blank&lt;/code&gt;, 空字符串也是 &lt;code&gt;blank?&lt;/code&gt; 的。然而，在上面这个示例中，&lt;code&gt;doc_method_id&lt;/code&gt; 的值只会是 &lt;code&gt;nil&lt;/code&gt; 或者是 Integer，这意味着 &lt;code&gt;present?&lt;/code&gt; 在逻辑上和 &lt;code&gt;!!&lt;/code&gt; 是相等的：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;doc_method?&lt;/span&gt;
  &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="n"&gt;doc_method_id&lt;/span&gt;
  &lt;span class="c1"&gt;# same as doc_method_id.present?&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种场景下 &lt;code&gt;present?&lt;/code&gt; 是错误的工具。如果你不关心你要断言的值是不是空的（比如 &lt;code&gt;[]&lt;/code&gt;, &lt;code&gt;{}&lt;/code&gt;），那就使用其他更简单更快速的语言特性。我甚至有时候还看到有人在一个布尔值上这样搞，这意味着你只是在增加冗余度，而且会让我感觉，这个地方是不是有什么我没想到的奇怪的 edge case?&lt;/p&gt;

&lt;p&gt;这些是我的观点，我理解你可能会不同意。&lt;code&gt;present?&lt;/code&gt; 在处理可能为空 (&lt;code&gt;""&lt;/code&gt;) 的字符串时比较适用。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;大家遇到问题的地方是调用断言方法的时候，比如 &lt;code&gt;present?&lt;/code&gt;, on ActiveRecord::Relation objects.&lt;/strong&gt; 我们假设现在你想知道一个 ActiveRecord::Relation 是否有数据，从英语语言角度上讲，你可以用同义词 any?/present?/exists? 或者是他们的反义词 none?/blank?/empty?. 你选哪个当然都没问题，对吧？挑一个你大声读出来最顺口的？No.&lt;/p&gt;

&lt;p&gt;你觉得下面的代码会执行怎样的 SQL？假设 &lt;code&gt;@comments&lt;/code&gt; is an ActiveRecord::Relation.&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- if @comments.any?
  h2 Comments on this Post
  - @comments.each do |comment|
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;答案是两条 SQL。首先是由 &lt;code&gt;@comments.any?&lt;/code&gt; 触发的存在性检查， &lt;code&gt;(SELECT 1 AS one FROM ... LIMIT 1)&lt;/code&gt;, 然后 &lt;code&gt;@comments.each&lt;/code&gt; 这一行会去 load 整个 relation &lt;code&gt;(SELECT "comments".* FROM "comments" WHERE ...)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What about this?&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- unless @comments.load.empty?
  h2 Comments on this Post
  - @comments.each do |comment|
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样只会执行一条 SQL - &lt;code&gt;@comments.load&lt;/code&gt; 会立即 load the entire relation with &lt;code&gt;SELECT "comments".* FROM "comments" WHERE ...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And this one?&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- if @comments.exists?
  This post has
  = @comments.size
  comments
- if @comments.exists?
  h2 Comments on this Post
  - @comments.each do |comment|
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;四次！&lt;code&gt;exists?&lt;/code&gt; 没有记忆功能 (doesn't memoize itself) 并且 不会 load the relation. 这里 &lt;code&gt;exists?&lt;/code&gt;  会触发 &lt;code&gt;SELECT 1 ...&lt;/code&gt;, &lt;code&gt;.size&lt;/code&gt; 会触发一次 &lt;code&gt;COUNT&lt;/code&gt;，因为 relation 还没有被加载。然后下一个 &lt;code&gt;exists?&lt;/code&gt; 会触发另一次 &lt;code&gt;SELECT 1 ...&lt;/code&gt;，最后，&lt;code&gt;@comments&lt;/code&gt; 会加载整个 relation! 耶！快乐吗？
你可以减少到 1 次查询，像这样：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- if @comments.load.any?
  This post has
  = @comments.size
  comments
- if @comments.any?
  h2 Comments on this Post
  - @comments.each do |comment|
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;取决于你的 Rails 版本，4.2 or 5.0 or 5.1+，行为会变。&lt;/p&gt;

&lt;p&gt;Here’s how it works in Rails 5.1+:&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2019/49753e4f-b242-4b59-abf6-00a06b1ca9fc.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;Here’s how it works in Rails 5.0:&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2019/ac4b7842-fbd3-4ca2-87ba-3dde6fa2251d.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;Here’s how it works in Rails 4.2:&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2019/c1bd5b94-2325-42d6-b664-8ccbbd63e84e.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;any?&lt;/code&gt;, &lt;code&gt;empty?&lt;/code&gt; 和 &lt;code&gt;none?&lt;/code&gt; 让我想起 &lt;code&gt;size&lt;/code&gt; 的实现 - 如果数据已经加载 (&lt;code&gt;loaded?&lt;/code&gt;)，简单地在已有数组上调一下方法就好；如果数据还没加载，就执行 SQL 查询。&lt;code&gt;exists?&lt;/code&gt; 和其他的 ActiveRecord::Calculations 一样，没有内置 caching 或者 memoization。这意味着在某些场景下， &lt;code&gt;exists?&lt;/code&gt; 这个人们喜欢用的方法，实际上比 &lt;code&gt;present?&lt;/code&gt; 要糟糕很多！&lt;/p&gt;

&lt;p&gt;这六个断言方法，在英语里都是问的同一个意思，却有着完全不同的实现和含义，并且结果会因你使用的 Rails 版本而异。所以，我把上面的东西提炼成一些具体的建议：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;如果你调了 &lt;code&gt;present?&lt;/code&gt; 或者 &lt;code&gt;blank?&lt;/code&gt; 之后并不会完全用到这个 ActiveRecord::Relation, 就不应该用 &lt;code&gt;present?&lt;/code&gt; 和 &lt;code&gt;blank?&lt;/code&gt;. 比如，&lt;code&gt;@my_relation.present?; @my_relation.first(3).each&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;any?&lt;/code&gt;, &lt;code&gt;none?&lt;/code&gt; and &lt;code&gt;empty?&lt;/code&gt; should probably be replaced with &lt;code&gt;present?&lt;/code&gt; or &lt;code&gt;blank?&lt;/code&gt; unless you will only take a section of the ActiveRecord::Relation using &lt;code&gt;first&lt;/code&gt; or &lt;code&gt;last&lt;/code&gt;. They will generate an extra existence SQL check if you’re just going to use the entire relation if it exists. In essence, change &lt;code&gt;@users.any?; @users.each...&lt;/code&gt; to &lt;code&gt;@users.present?; @users.each...&lt;/code&gt; or &lt;code&gt;@users.load.any?; @users.each...&lt;/code&gt;, but &lt;code&gt;@users.any?; @users.first(3).each&lt;/code&gt; is fine.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;exists?&lt;/code&gt; is a lot like &lt;code&gt;count&lt;/code&gt; - it is never memoized, and always executes a SQL query. Most people probably do not actually want this behavior, and would be better off using &lt;code&gt;present?&lt;/code&gt; or &lt;code&gt;blank?&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h6 id="　"&gt; &lt;/h6&gt;&lt;h2 id="Conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;随着你的应用的大小和复杂度不断增加，不必要的 SQL 会给你的应用表现拖后腿。每一次 SQL 查询都是和数据库的交互 (round-trip back to the database)，通常至少需要 1ms, 对于复杂的 &lt;code&gt;WHERE&lt;/code&gt; 语句会更多。虽然一次额外的 &lt;code&gt;exists?&lt;/code&gt; 检查没什么大问题，但如果这突然发生在一个表的每一行，或者一个 collection 里的 partial 里，你就摊上事了！&lt;/p&gt;

&lt;p&gt;ActiveRecord 是一个很牛逼的抽象，但是数据库访问毕竟不是"无门槛的"，我们需要清楚 ActiveRecord 内部是怎么工作的才能避免不必要的数据库访问。&lt;/p&gt;
&lt;h6 id="　"&gt; &lt;/h6&gt;&lt;h2 id="App Checklist"&gt;App Checklist&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Look for uses of &lt;code&gt;present?&lt;/code&gt;, &lt;code&gt;none?&lt;/code&gt;, &lt;code&gt;any?&lt;/code&gt;, &lt;code&gt;blank?&lt;/code&gt; and &lt;code&gt;empty?&lt;/code&gt; on objects which may be ActiveRecord::Relations. Are you just going to load the entire array later if the relation is present? If so, add load to the call (e.g. &lt;code&gt;@my_relation.load.any?&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;小心地使用 &lt;code&gt;exists?&lt;/code&gt; - 它&lt;em&gt;总会&lt;/em&gt;执行 SQL 查询。只在合适的场景下使用它 - 其他情况就使用 &lt;code&gt;present?&lt;/code&gt; 或者其他调用 &lt;code&gt;empty?&lt;/code&gt; 的方法&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Be extremely careful using &lt;code&gt;where&lt;/code&gt; in instance methods on ActiveRecord objects - they break preloading and often cause N+1s when used in rendering collections.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;count&lt;/code&gt; always executes a SQL query - audit its use in your codebase, and determine if a &lt;code&gt;size&lt;/code&gt; check would be more appropriate.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>rennyallen</author>
      <pubDate>Fri, 01 Mar 2019 13:35:38 +0800</pubDate>
      <link>https://ruby-china.org/topics/38174</link>
      <guid>https://ruby-china.org/topics/38174</guid>
    </item>
    <item>
      <title>两个页面显示问题反馈</title>
      <description>&lt;p&gt;1. 如图所示，在“管理我的应用”页面，整个 container 的位置都不对，上面部分没显示出来。另外这个页面的整个导航栏样式也和其他页面不一样。
&lt;img src="https://l.ruby-china.com/photo/2018/e7d60c02-c17f-4da2-8a6a-71b477d9074c.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;2. 在预览编辑时文本会溢出，这个已提交 PR: &lt;a href="https://github.com/ruby-china/homeland/pull/1035" rel="nofollow" target="_blank" title=""&gt;https://github.com/ruby-china/homeland/pull/1035&lt;/a&gt;
&lt;img src="https://l.ruby-china.com/photo/2018/2ac3c224-156e-47d1-abb2-9938303f3472.png!large" title="" alt=""&gt;&lt;/p&gt;</description>
      <author>rennyallen</author>
      <pubDate>Wed, 09 May 2018 11:28:31 +0800</pubDate>
      <link>https://ruby-china.org/topics/36726</link>
      <guid>https://ruby-china.org/topics/36726</guid>
    </item>
    <item>
      <title>怎么在删除话题的时候关联删除话题里的资源文件？</title>
      <description>&lt;p&gt;比如： &lt;/p&gt;

&lt;p&gt;我在 ruby-china 发个帖，我点击 &lt;img src="https://l.ruby-china.com/photo/2017/74b716ea-0793-4b9a-81b4-861ce429f44d.png!large" title="" alt=""&gt; 上传了一张图片，这时候会向后台发送一个 POST 请求，把这张图片上传到服务器，然后写入数据库（比如 ruby-china 里面是 photos 这张表）。&lt;/p&gt;

&lt;p&gt;好，现在我突然决定不发帖了，退出页面了，或者说我发帖之后把这个帖子删了，但是这张图片依然存在服务器和数据库里面，而这里面的图片已经没用了，有什么办法删掉它吗？&lt;/p&gt;

&lt;p&gt;思考：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;我看 ruby-china 的 photo 好像是和 user 做的关联&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Photo&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么如果有个删帖的功能（真删除），帖子删了之后这些相关图片还是存在的吧？那这样累计下去不会占用很多空间吗？或者说我就一直点上传图片，传很多，但我不发帖，那这些图片有什么办法删掉？&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;和 topics 表做关联，然后 dependent: :destroy？&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这样做的话，那么在 photos 表里面应该有个 topic_id 的字段吧，那么，我在发帖的时候 (topics/new) 这时候新话题还没存入数据库，哪里有 id？
也就是说当上传图片的时候，POST 请求过去，然后执行&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="vi"&gt;@photo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
&lt;span class="vi"&gt;@photo.topic_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sc"&gt;??&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 topic 的 id 要等表单提交之后，执行 create action 之后才有 id 呀&lt;/p&gt;

&lt;p&gt;然后我就想，那么我在 create topic 之后去修改 photo 的 topic_id? 这时候那一堆 photo 和这个 topic 已经没有关联了啊，那我还怎么能找到这些个指定的 photo?&lt;/p&gt;

&lt;p&gt;我不知道我表达清楚没，这东西和帖子&amp;amp;回复啥的不一样，topic has many replies, reply belongs to topic, 在创建回复的时候可以找到这个 topic, 就能 topic.replies.create，但是这个是在 topic 没有创建的时候去操作，创建 photo 需要 topic, 而 topic 还没创建，等 topic 创建了，photo 又已经创建好了，我想给 photo 加上 topic_id，又需要这个 topic_id 才能找到 photo, 这不是个悖论吗？（当然如果是 &lt;strong&gt;edit&lt;/strong&gt; topic(topics/22/edit)，然后上传图片，这样是没问题）&lt;/p&gt;

&lt;p&gt;其实我是在用 &lt;a href="https://github.com/Macrow/rails_kindeditor/tree/master" rel="nofollow" target="_blank" title=""&gt;rails_kindeditor&lt;/a&gt; 这个 gem，它里面要求的一个 owner_id 就是事先被创建的而且不能是空的，所以就遇到这个问题了&lt;/p&gt;

&lt;p&gt;最后总结一下，转化为一个问题就是 &lt;strong&gt;怎么能把 topic 和 上传的图片 关联起来？&lt;/strong&gt;&lt;/p&gt;</description>
      <author>rennyallen</author>
      <pubDate>Mon, 20 Nov 2017 23:34:51 +0800</pubDate>
      <link>https://ruby-china.org/topics/34599</link>
      <guid>https://ruby-china.org/topics/34599</guid>
    </item>
    <item>
      <title>[译] 关于 RSpec 的一点方法总结</title>
      <description>&lt;p&gt;两个主要的框架垄断了 Ruby 的测试界：Rspec 和 MiniTest. Rspec 是一个非常有表现力的测试框架，它有很多好的特性和辅助方法来让测试变得可读。当我们用 Rspec 写测试的时候，有几个小的方法，或许可以让测试更好写、更易读、更利于维护。&lt;/p&gt;

&lt;p&gt;现在假设有一个系统，有书 (&lt;code&gt;Books&lt;/code&gt;) 和 作者 (&lt;code&gt;Authors&lt;/code&gt;)，让我们使用一些方法来简化测试。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Book&lt;/span&gt;
  &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:genre&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;genre&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;
    &lt;span class="vi"&gt;@genre&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;genre&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Author&lt;/span&gt;
  &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:books&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;
    &lt;span class="vi"&gt;@books&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;books&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;has_written_a_book?&lt;/span&gt;
    &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;books&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h6 id="　"&gt; &lt;/h6&gt;&lt;h3 id="let 和 subject"&gt;let 和 subject&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;let()&lt;/code&gt; 有两个好处：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;不用赋值给实例变量就可以缓存值;&lt;/li&gt;
&lt;li&gt;定义的变量是“惰性计算”的，不调用就不会执行赋值操作;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;subject{}&lt;/code&gt; 可用来声明测试的对象，后续的测试用例就无需明确指定了。&lt;/p&gt;

&lt;p&gt;通过声明 &lt;code&gt;let&lt;/code&gt; 和 &lt;code&gt;subject&lt;/code&gt; 变量是一个保持测试可读性和不重复自己 (DRY) 的好方法。&lt;/p&gt;

&lt;p&gt;举个例子，如果我们想确认一个作者有名字 (assert an &lt;code&gt;Author&lt;/code&gt; has a &lt;code&gt;name&lt;/code&gt;)，如果不用 &lt;code&gt;let&lt;/code&gt; 和 &lt;code&gt;subject&lt;/code&gt;变量，测试大概长这样：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="no"&gt;Author&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="vi"&gt;@book_genre&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Historical Fiction'&lt;/span&gt;
    &lt;span class="vi"&gt;@book_title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'A Tale of Two Cities'&lt;/span&gt;
    &lt;span class="vi"&gt;@book&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Book&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@book_genre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@book_title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@author_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Charles Dickens'&lt;/span&gt;
    &lt;span class="vi"&gt;@author&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@author_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="vi"&gt;@book&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s1"&gt;'#name'&lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'has a name set'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@author.name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@author_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果再加上别的测试，比如确认书的数量，不同的名字，或者其他关于这个作者的东西的话，这测试就会变得很冗长。&lt;/p&gt;

&lt;p&gt;所以，我们可以使用 &lt;code&gt;let&lt;/code&gt; 和 &lt;code&gt;subject&lt;/code&gt; 变量来实现 DRY:&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="no"&gt;Author&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:book_genre&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'Historical Fiction'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:book_title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'A Tale of Two Cities'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:book&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;Book&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;book_genre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;book_title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:book_array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:author_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'Charles Dickens'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;subject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;Author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;author_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;book_array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s1"&gt;'#name'&lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'has a name set'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;author_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s1"&gt;'#books'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="s1"&gt;'with books'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'has books set'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;books&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;book_array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="s1"&gt;'without books'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="s1"&gt;'books variable is nil'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:book_array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'sets books to an empty array'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
          &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;books&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="s1"&gt;'books variable is an empty array'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:book_array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'sets books to an empty array'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
          &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;books&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不再需要几个 &lt;code&gt;before&lt;/code&gt; 块来定义一个个实例变量，这段代码用了 &lt;code&gt;let&lt;/code&gt;，简洁易读。更具体地说，这些测试的工作机制是：每当一个 &lt;code&gt;it&lt;/code&gt; 块运行的时候，&lt;code&gt;context&lt;/code&gt;里面离得最近的 &lt;code&gt;let&lt;/code&gt;就被用来初始化这个 &lt;code&gt;subject&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;通过设置 let 变量，如果想要测试 &lt;code&gt;subject.books&lt;/code&gt;是不是一个数组，判断输入是&lt;code&gt;nil&lt;/code&gt;或者&lt;code&gt;[]&lt;/code&gt;，只需要简单地修改一下 &lt;code&gt;let&lt;/code&gt; 声明：&lt;code&gt;let(:book_array) { nil }&lt;/code&gt;&lt;/p&gt;
&lt;h6 id="　"&gt; &lt;/h6&gt;&lt;h3 id="Loose Expectations"&gt;Loose Expectations&lt;/h3&gt;
&lt;p&gt;如果一个测试不关心细节，Rspec 允许使用 general expectations 和 占位符 (placeholders). 这些占位符可以只专注于真正重要的东西，从而简化测试。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;anything&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;正如其名，当一个方法要求一个参数但是这个具体的参数又对测试没有影响的时候，我们可以使用 &lt;code&gt;anything&lt;/code&gt; 这个参数匹配符。&lt;/p&gt;

&lt;p&gt;如果一个测试，想要确认一个作者已经写过书了，但不关心这本书的标题和类型，就可以用 &lt;code&gt;anything&lt;/code&gt; ：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="no"&gt;Author&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s1"&gt;'#has_written_a_book?'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="s1"&gt;'when books are passed in'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;subject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;Author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:books&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;Book&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;anything&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;anything&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'is true'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has_written_a_book?&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;hash_including&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;当我们要测试一个期望是 &lt;code&gt;Hash&lt;/code&gt; 的方法，这个 &lt;code&gt;Hash&lt;/code&gt; 中的某些元素可能比其他的元素更重要。&lt;code&gt;hash_including&lt;/code&gt; 匹配符允许开发者 assert 一个或多个 hash 的键值对，而不用指定整个 hash。&lt;/p&gt;

&lt;p&gt;假设 &lt;code&gt;Book&lt;/code&gt; 类有一个方法，实例化了一个 HTTP client (用来取得一些附加信息)： &lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Book&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_information&lt;/span&gt;
    &lt;span class="no"&gt;HTTPClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="ss"&gt;title: &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;genre: &lt;/span&gt;&lt;span class="n"&gt;genre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;time: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
              &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/information'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;给这个方法写测试应该 assert 这个 client 已经初始化了几个关键点，这个场景就可以用 &lt;code&gt;hash_including&lt;/code&gt;。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="no"&gt;Book&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s1"&gt;'#fetch_information'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:book_genre&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'Historical Fiction'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:book_title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'A Tale of Two Cities'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;subject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;Book&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;genre&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'instantiates the client correctly'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;HTTPClient&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;receive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hash_including&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;title: &lt;/span&gt;&lt;span class="n"&gt;book_title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                             &lt;span class="ss"&gt;genre: &lt;/span&gt;&lt;span class="n"&gt;book_genre&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch_information&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;hash_including&lt;/code&gt;可以指定一个期望的 hash 中的键值对或者只是键。这里，这个测试只关心传入的书的标题 (&lt;code&gt;title&lt;/code&gt;) 和类型 (&lt;code&gt;genre&lt;/code&gt;)。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;match_array&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;在 Ruby 中，当且仅当两个数组包含同样的元素且顺序相同时，这两个数组是相等 (equal) 的。在一些测试中，这个严格相等的准则可能不是必须的。那些情况下，RSpec 提供了一个叫做 &lt;code&gt;match_array&lt;/code&gt;的方法来让测试顺利进行。&lt;/p&gt;

&lt;p&gt;如果 &lt;code&gt;Author&lt;/code&gt; 类从数据库中取得了它的书的清单，由于默认的 scopes 或者记录的更新操作，书的顺序可能不是连续的。&lt;/p&gt;

&lt;p&gt;现定义一个 &lt;code&gt;fetch_books&lt;/code&gt; 方法：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Author&lt;/span&gt;
  &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_books&lt;/span&gt;
    &lt;span class="no"&gt;BookDB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;author_name: &lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;match_array&lt;/code&gt;，测试可以确认返回了合适的书，无视顺序：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="no"&gt;Author&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s1"&gt;'#fetch_books'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'Jane Austen'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;let!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:books&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="no"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="no"&gt;BookDB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_book&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;author_name: &lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;subject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;Author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'fetches the books correctly'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch_books&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;match_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;books&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（&lt;a href="http://jakeyesbeck.com/2017/07/12/a-few-rspec-helpful-hints/?utm_source=rubyweekly&amp;amp;utm_medium=email" rel="nofollow" target="_blank" title=""&gt;原文&lt;/a&gt;翻译时有删改）&lt;/p&gt;</description>
      <author>rennyallen</author>
      <pubDate>Sun, 23 Jul 2017 17:34:56 +0800</pubDate>
      <link>https://ruby-china.org/topics/33592</link>
      <guid>https://ruby-china.org/topics/33592</guid>
    </item>
    <item>
      <title>在 Aliyun ECS 上用 Nginx + Passenger 部署 Rails 应用时安装 Passenger 的问题</title>
      <description>&lt;p&gt;第一次部署 Rails 应用，参照&lt;a href="https://github.com/ruby-china/homeland/wiki/Ubuntu-12.04-%E4%B8%8A%E4%BD%BF%E7%94%A8-Nginx-Passenger-%E9%83%A8%E7%BD%B2-Ruby-on-Rails" rel="nofollow" target="_blank" title=""&gt;资料 1&lt;/a&gt;  &lt;a href="https://ruby-china.org/wiki/deploy-rails-on-ubuntu-server" title=""&gt;资料 2&lt;/a&gt;  &lt;a href="https://github.com/huacnlee/init.d" rel="nofollow" target="_blank" title=""&gt;资料 3&lt;/a&gt;  &lt;a href="https://github.com/ruby-china/homeland/wiki/%E5%A6%82%E4%BD%95%E5%AE%89%E8%A3%85-Rails-%E7%94%9F%E4%BA%A7%E7%8E%AF%E5%A2%83" rel="nofollow" target="_blank" title=""&gt;资料 4&lt;/a&gt;等资料在服务器上装好了 ruby, rvm, 相关依赖等，但在安装 passenger 时，参照&lt;a href="https://www.phusionpassenger.com/library/install/nginx/install/oss/xenial/" rel="nofollow" target="_blank" title=""&gt;此官方文档&lt;/a&gt;，安装了秘钥和 HTTP 支持，也添加了 APT 源：
&lt;code&gt;
sudo sh -c 'echo deb https://oss-binaries.phusionpassenger.com/apt/passenger xenial main &amp;gt; /etc/apt/sources.list.d/passenger.list'
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;但在执行 &lt;code&gt;sudo apt-get update&lt;/code&gt;时遇到以下错误：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hit:1 http://mirrors.cloud.aliyuncs.com/ubuntu xenial InRelease
Hit:2 http://mirrors.aliyun.com/ubuntu xenial InRelease
Get:3 http://mirrors.cloud.aliyuncs.com/ubuntu xenial-security InRelease [102 kB]
Get:4 http://mirrors.aliyun.com/ubuntu xenial-security InRelease [102 kB]      
Get:5 http://mirrors.cloud.aliyuncs.com/ubuntu xenial-updates InRelease [102 kB]
Get:6 http://mirrors.aliyun.com/ubuntu xenial-updates InRelease [102 kB]       
Get:7 http://mirrors.cloud.aliyuncs.com/ubuntu xenial-proposed InRelease [253 kB]
                                     ……
                                     ……
Get:45 http://mirrors.aliyun.com/ubuntu xenial-proposed/universe i386 Packages [31.6 kB]
Get:46 http://mirrors.aliyun.com/ubuntu xenial-proposed/universe Translation-en [12.9 kB]
Ign:47 https://oss-binaries.phusionpassenger.com/apt/passenger xenial InRelease 
Ign:48 https://oss-binaries.phusionpassenger.com/apt/passenger xenial Release   
Ign:49 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main amd64 Packages
Ign:50 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main i386 Packages 
Ign:51 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main all Packages
Ign:52 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main Translation-en_US
Ign:53 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main Translation-en
Ign:49 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main amd64 Packages
Ign:50 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main i386 Packages
Ign:51 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main all Packages
Ign:52 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main Translation-en_US
Ign:53 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main Translation-en
Ign:49 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main amd64 Packages
Ign:50 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main i386 Packages
Ign:51 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main all Packages
Ign:52 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main Translation-en_US
Ign:53 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main Translation-en
Ign:49 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main amd64 Packages
Ign:50 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main i386 Packages
Ign:51 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main all Packages
Ign:52 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main Translation-en_US
Ign:53 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main Translation-en
Ign:49 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main amd64 Packages
Ign:50 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main i386 Packages
Ign:51 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main all Packages
Ign:52 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main Translation-en_US
Ign:53 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main Translation-en
Err:49 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main amd64 Packages
  Bad header line
Ign:50 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main i386 Packages
Ign:51 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main all Packages
Ign:52 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main Translation-en_US
Ign:53 https://oss-binaries.phusionpassenger.com/apt/passenger xenial/main Translation-en
Fetched 6,467 kB in 2min 3s (52.4 kB/s)
Reading package lists... Done
W: The repository 'https://oss-binaries.phusionpassenger.com/apt/passenger xenial Release' does not have a Release file.
N: Data from such a repository can't be authenticated and is therefore potentially dangerous to use.
N: See apt-secure(8) manpage for repository creation and user configuration details.
E: Failed to fetch https://oss-binaries.phusionpassenger.com/apt/passenger/dists/xenial/main/binary-amd64/Packages  Bad header line
E: Some index files failed to download. They have been ignored, or old ones used instead.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与 passenger 相关的都是 Ign 或者 Err 了，然后报的&lt;code&gt;Bad header line&lt;/code&gt;也不知道是怎么回事，总之到&lt;code&gt;apt-get update&lt;/code&gt;这步永远也不能成功执行。&lt;/p&gt;

&lt;p&gt;服务器系统是 Ubuntu 16.04 LTS，我在本地物理机上相同系统也试了，没有出现这个问题，passenger 可以成功安装，所以应该不是服务器系统或者 APT 源的问题。有人遇到过这种情况吗，google, stackoverflow 试了各种方法折腾了一天一夜也没解决，准备放弃 改用 Puma 了。BTW，用老的方法&lt;code&gt;passenger-install-nginx-module&lt;/code&gt;也不行，downloading  Nginx 之后就会卡住不动了。&lt;/p&gt;</description>
      <author>rennyallen</author>
      <pubDate>Wed, 15 Feb 2017 15:56:25 +0800</pubDate>
      <link>https://ruby-china.org/topics/32310</link>
      <guid>https://ruby-china.org/topics/32310</guid>
    </item>
    <item>
      <title>Why Is 'sum' So Much Faster Than inject (:+)?</title>
      <description>&lt;p&gt;刚看到个&lt;a href="http://stackoverflow.com/questions/41449617/why-is-sum-so-much-faster-than-inject" rel="nofollow" target="_blank" title=""&gt;问题&lt;/a&gt;&lt;br&gt;
感觉有点意思，分享给大家。让我想起了高中数学&lt;/p&gt;</description>
      <author>rennyallen</author>
      <pubDate>Fri, 13 Jan 2017 18:23:04 +0800</pubDate>
      <link>https://ruby-china.org/topics/32122</link>
      <guid>https://ruby-china.org/topics/32122</guid>
    </item>
    <item>
      <title>Rails 中的抽象类 (abstract_class) 到底是什么意思？</title>
      <description>&lt;p&gt;发现每个 Rails 项目中默认都有这么一句：&lt;code&gt;self.abstract_class = true&lt;/code&gt;，这貌似是&lt;a href="https://ruby-china.org/topics/30711" title=""&gt;rails 5 新增加的东西&lt;/a&gt;？&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/application_record.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationRecord&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abstract_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;搜了一下，有用的信息不多：&lt;/p&gt;
&lt;h6 id="“子类继承的时候不能继承父类的隐式属性”"&gt;“子类继承的时候不能继承父类的隐式属性”&lt;/h6&gt;&lt;h6 id="“只对你自己的程序起作用，从而不会污染其他插件”"&gt;“只对你自己的程序起作用，从而不会污染其他插件”&lt;/h6&gt;&lt;h6 id=""&gt;"setting self.abstract_class = true meant that your model did not have a table backing it."&lt;/h6&gt;
&lt;p&gt;新手看得迷迷糊糊，求解：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;self.abstract_class = true&lt;/code&gt;这句是什么意思？为什么要把 self.abstract_class 设置为 true？可以举个例子吗；&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;是不是设置了这个抽象类我就可以创建一个数据库里没有这个表的 AR model 了？如果是的话，那这样做有什么意义？AR 不就是要把对象映射到数据库中对应的表吗，所以又回到了第一个问题。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <author>rennyallen</author>
      <pubDate>Wed, 11 Jan 2017 00:38:16 +0800</pubDate>
      <link>https://ruby-china.org/topics/32100</link>
      <guid>https://ruby-china.org/topics/32100</guid>
    </item>
    <item>
      <title>那些降低数据库性能的 Rails 风格 (Common Rails Idioms that Kill Database Performance)</title>
      <description>&lt;h2 id="前言"&gt;前言&lt;/h2&gt;
&lt;p&gt;关于数据库查询，让我们来看一看那些让你的程序停滞不前的罪魁祸首。&lt;/p&gt;

&lt;p&gt;我还记得我第一次看到 Rails 的 ActiveRecord，那是一次启发。那是 2005 年的时候，当时我在给一个 PHP 程序写 SQL 语句。突然间，写数据库从单调繁杂的零星工作变得简单且有趣了。&lt;/p&gt;

&lt;p&gt;...然后我就开始关注到性能 (performance) 的问题。&lt;/p&gt;

&lt;p&gt;ActiveRecord 它本身并不慢。我停止把注意力花在那些实际正在运行的查询上。结果是，当数据量变得庞大后，在 rails 的增删查改操作中一些最符合语言习惯的数据库查询就十分低效。&lt;/p&gt;

&lt;p&gt;这篇文章我们将讨论这其中的三个罪魁祸首。但首先，让我们先聊聊怎么知道你的数据库查询是否高效。&lt;/p&gt;
&lt;h5 id="　　"&gt;  &lt;/h5&gt;&lt;h2 id="测量性能"&gt;测量性能&lt;/h2&gt;
&lt;p&gt;如果你数据量足够小的话每个数据库查询都是高效的。所以为了真正地感知效率，我们需要以一个生产级别的数据库为基准。在我们的示例中，我们将使用一个有大约 22000 条记录的叫做 faults 的表。&lt;/p&gt;

&lt;p&gt;我们会用到 postgres。在 postgres 中，衡量性能的方法是使用 explain。比如：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# explain (analyze) select * from faults where id = 1;
                                     QUERY PLAN
--------------------------------------------------------------------------------------------------
 Index Scan using faults_pkey on faults  (cost=0.29..8.30 rows=1 width=1855) (actual time=0.556..0.556 rows=0 loops=1)
   Index Cond: (id = 1)
 Total runtime: 0.626 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这显示出了执行这次查询的预计花费 (cost=0.29..8.30 rows=1 width=1855) 和执行的实际时间 (actual time=0.556..0.556 rows=0 loops=1)。&lt;/p&gt;

&lt;p&gt;如果你想要一个更易读的格式，你可以让 postgres 以 YAML 样式 打印出来。&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# explain (analyze, format yaml) select * from faults where id = 1;&lt;/span&gt;
              &lt;span class="s"&gt;QUERY PLAN&lt;/span&gt;
&lt;span class="s"&gt;--------------------------------------&lt;/span&gt;
 &lt;span class="s"&gt;- Plan&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;                             &lt;span class="s"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;Node Type&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Index&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Scan"&lt;/span&gt;         &lt;span class="s"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;Scan Direction&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Forward"&lt;/span&gt;       &lt;span class="s"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;Index Name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;faults_pkey"&lt;/span&gt;       &lt;span class="s"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;Relation Name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;faults"&lt;/span&gt;         &lt;span class="s"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;Alias&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;faults"&lt;/span&gt;                 &lt;span class="s"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;Startup Cost&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.29              +&lt;/span&gt;
     &lt;span class="s"&gt;Total Cost&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;8.30                +&lt;/span&gt;
     &lt;span class="s"&gt;Plan Rows&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1                    +&lt;/span&gt;
     &lt;span class="s"&gt;Plan Width&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1855                +&lt;/span&gt;
     &lt;span class="s"&gt;Actual Startup Time&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.008      +&lt;/span&gt;
     &lt;span class="s"&gt;Actual Total Time&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.008        +&lt;/span&gt;
     &lt;span class="s"&gt;Actual Rows&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0                  +&lt;/span&gt;
     &lt;span class="s"&gt;Actual Loops&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1                 +&lt;/span&gt;
     &lt;span class="s"&gt;Index Cond&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(id&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1)"&lt;/span&gt;          &lt;span class="s"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;Rows Removed by Index Recheck&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0+&lt;/span&gt;
   &lt;span class="s"&gt;Triggers&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;                         &lt;span class="s"&gt;+&lt;/span&gt;
   &lt;span class="s"&gt;Total Runtime&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.036&lt;/span&gt;
&lt;span class="s"&gt;(1 row)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们只需要关注 "Plan Rows" 和 "Actual Rows"&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Plan Rows 在最坏的情况下，数据库需要循环多少行来响应你的查询&lt;/li&gt;
&lt;li&gt;Actual Rows 当它执行这次查询时，数据库实际循环了多少行？&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果 "Plan Rows" 是 1，就像上边这样，那么这次查询可能是高效的。如果 "Plan Rows" 等于这个数据库的行数，那么意味着这次查询将会做一个“全表扫描”，并不够好。&lt;/p&gt;

&lt;p&gt;既然你知道了怎样来测量查询的效率，那我们来看一些常规的 rails 语句，看它们是怎么运行的。&lt;/p&gt;
&lt;h5 id="　　"&gt;  &lt;/h5&gt;&lt;h2 id="计数(Counting)"&gt;计数 (Counting)&lt;/h2&gt;
&lt;p&gt;在 Rails views 中经常看到这样的代码：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Total&lt;/span&gt; &lt;span class="no"&gt;Faults&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= Fault.count %&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应的 SQL 为：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;select count(*) from faults;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;让我们把它放到&lt;code&gt;explain&lt;/code&gt;里看看会发生什么。&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# explain (analyze, format yaml) select count(*) from faults;&lt;/span&gt;
              &lt;span class="s"&gt;QUERY PLAN&lt;/span&gt;
&lt;span class="s"&gt;--------------------------------------&lt;/span&gt;
 &lt;span class="s"&gt;- Plan&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;                             &lt;span class="s"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;Node Type&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Aggregate"&lt;/span&gt;          &lt;span class="s"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;Strategy&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Plain"&lt;/span&gt;               &lt;span class="s"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;Startup Cost&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1840.31           +&lt;/span&gt;
     &lt;span class="s"&gt;Total Cost&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1840.32             +&lt;/span&gt;
     &lt;span class="s"&gt;Plan Rows&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1                    +&lt;/span&gt;
     &lt;span class="s"&gt;Plan Width&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0                   +&lt;/span&gt;
     &lt;span class="s"&gt;Actual Startup Time&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;24.477     +&lt;/span&gt;
     &lt;span class="s"&gt;Actual Total Time&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;24.477       +&lt;/span&gt;
     &lt;span class="s"&gt;Actual Rows&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1                  +&lt;/span&gt;
     &lt;span class="s"&gt;Actual Loops&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1                 +&lt;/span&gt;
     &lt;span class="s"&gt;Plans&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;                          &lt;span class="s"&gt;+&lt;/span&gt;
       &lt;span class="s"&gt;- Node Type&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Seq&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Scan"&lt;/span&gt;       &lt;span class="s"&gt;+&lt;/span&gt;
         &lt;span class="s"&gt;Parent Relationship&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Outer"+&lt;/span&gt;
         &lt;span class="s"&gt;Relation Name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;faults"&lt;/span&gt;     &lt;span class="s"&gt;+&lt;/span&gt;
         &lt;span class="s"&gt;Alias&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;faults"&lt;/span&gt;             &lt;span class="s"&gt;+&lt;/span&gt;
         &lt;span class="s"&gt;Startup Cost&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.00          +&lt;/span&gt;
         &lt;span class="s"&gt;Total Cost&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1784.65         +&lt;/span&gt;
         &lt;span class="s"&gt;Plan Rows&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;22265            +&lt;/span&gt;
         &lt;span class="s"&gt;Plan Width&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0               +&lt;/span&gt;
         &lt;span class="s"&gt;Actual Startup Time&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.311  +&lt;/span&gt;
         &lt;span class="s"&gt;Actual Total Time&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;22.839   +&lt;/span&gt;
         &lt;span class="s"&gt;Actual Rows&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;22265          +&lt;/span&gt;
         &lt;span class="s"&gt;Actual Loops&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1             +&lt;/span&gt;
   &lt;span class="s"&gt;Triggers&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;                         &lt;span class="s"&gt;+&lt;/span&gt;
   &lt;span class="s"&gt;Total Runtime&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="m"&gt;24.555&lt;/span&gt;
&lt;span class="s"&gt;(1 row)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们的示例 count 查询循环了 22265 行 — 整个表！在 postgres 中，counts 总是循环整个数据集。
你可以通过加上&lt;code&gt;where&lt;/code&gt;条件来减少数据集的大小。取决于你的要求，你可以减到足够小直到效率可以接受。&lt;/p&gt;

&lt;p&gt;另外一个解决此问题的方法是把你的 count 值缓存起来。&lt;a href="https://ruby-china.org/topics/32073" title=""&gt;你可以这样来做：&lt;/a&gt;&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:counter_cache&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外，当去检查这次查询是否返回任何数据时，用&lt;code&gt;Users.exists?&lt;/code&gt;而不是&lt;code&gt;Users.count&amp;gt;0&lt;/code&gt;。这样查询的结果更加高效。&lt;/p&gt;
&lt;h5 id="　　"&gt;  &lt;/h5&gt;&lt;h2 id="排序(Sorting)"&gt;排序 (Sorting)&lt;/h2&gt;
&lt;p&gt;几乎每个程序都至少有一个 index 页面，你从数据库里面读取了最新的 20 条记录然后展示出来。怎样做更简单？&lt;/p&gt;

&lt;p&gt;读数据的代码大概像这样：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="vi"&gt;@faults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Fault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;created_at: :desc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应的 sql 为：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;select * from faults order by created_at desc;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么我们来分析一下：&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# explain (analyze, format yaml) select * from faults order by created_at desc;&lt;/span&gt;
              &lt;span class="s"&gt;QUERY PLAN&lt;/span&gt;
&lt;span class="s"&gt;--------------------------------------&lt;/span&gt;
 &lt;span class="s"&gt;- Plan&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;                             &lt;span class="s"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;Node Type&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sort"&lt;/span&gt;               &lt;span class="s"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;Startup Cost&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;39162.46          +&lt;/span&gt;
     &lt;span class="s"&gt;Total Cost&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;39218.12            +&lt;/span&gt;
     &lt;span class="s"&gt;Plan Rows&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;22265                +&lt;/span&gt;
     &lt;span class="s"&gt;Plan Width&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1855                +&lt;/span&gt;
     &lt;span class="s"&gt;Actual Startup Time&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;75.928     +&lt;/span&gt;
     &lt;span class="s"&gt;Actual Total Time&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;86.460       +&lt;/span&gt;
     &lt;span class="s"&gt;Actual Rows&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;22265              +&lt;/span&gt;
     &lt;span class="s"&gt;Actual Loops&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1                 +&lt;/span&gt;
     &lt;span class="s"&gt;Sort Key&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;                       &lt;span class="s"&gt;+&lt;/span&gt;
       &lt;span class="s"&gt;- "created_at"                +&lt;/span&gt;
     &lt;span class="s"&gt;Sort Method&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;external&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;merge"&lt;/span&gt;   &lt;span class="s"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;Sort Space Used&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10752          +&lt;/span&gt;
     &lt;span class="s"&gt;Sort Space Type&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Disk"&lt;/span&gt;         &lt;span class="s"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;Plans&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;                          &lt;span class="s"&gt;+&lt;/span&gt;
       &lt;span class="s"&gt;- Node Type&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Seq&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Scan"&lt;/span&gt;       &lt;span class="s"&gt;+&lt;/span&gt;
         &lt;span class="s"&gt;Parent Relationship&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Outer"+&lt;/span&gt;
         &lt;span class="s"&gt;Relation Name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;faults"&lt;/span&gt;     &lt;span class="s"&gt;+&lt;/span&gt;
         &lt;span class="s"&gt;Alias&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;faults"&lt;/span&gt;             &lt;span class="s"&gt;+&lt;/span&gt;
         &lt;span class="s"&gt;Startup Cost&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.00          +&lt;/span&gt;
         &lt;span class="s"&gt;Total Cost&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1784.65         +&lt;/span&gt;
         &lt;span class="s"&gt;Plan Rows&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;22265            +&lt;/span&gt;
         &lt;span class="s"&gt;Plan Width&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1855            +&lt;/span&gt;
         &lt;span class="s"&gt;Actual Startup Time&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.004  +&lt;/span&gt;
         &lt;span class="s"&gt;Actual Total Time&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;4.653    +&lt;/span&gt;
         &lt;span class="s"&gt;Actual Rows&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;22265          +&lt;/span&gt;
         &lt;span class="s"&gt;Actual Loops&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1             +&lt;/span&gt;
   &lt;span class="s"&gt;Triggers&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;                         &lt;span class="s"&gt;+&lt;/span&gt;
   &lt;span class="s"&gt;Total Runtime&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="m"&gt;102.288&lt;/span&gt;
&lt;span class="s"&gt;(1 row)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们可以看到，每次你做这个查询时数据库会对所有的 22265 行进行排序。这样可不好。&lt;/p&gt;

&lt;p&gt;默认情况下，SQL 中的每个&lt;code&gt;order_by&lt;/code&gt;语句都会实时地对数据集排序，没有缓存。&lt;/p&gt;

&lt;p&gt;解决方法是使用索引。像这种简单的情况，加一个 sorted index 到 created_at column 中将会大大提高查询速度。&lt;/p&gt;

&lt;p&gt;在你的 Rails migration 中你可以：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AddIndexToFaultCreatedAt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
    &lt;span class="n"&gt;add_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:faults&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这会运行以下的 SQL:&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE INDEX index_faults_on_created_at ON faults USING btree (created_at);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里最后的&lt;code&gt;created_at&lt;/code&gt;指的是排列顺序，默认是升序。&lt;/p&gt;

&lt;p&gt;现在我们再运行一下排序的查询，我们可以看到不再包含一个排序的步骤了，只是简单地从 index 里读取已经排序好的数据。&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# explain (analyze, format yaml) select * from faults order by created_at desc;&lt;/span&gt;
                  &lt;span class="s"&gt;QUERY PLAN&lt;/span&gt;
&lt;span class="s"&gt;----------------------------------------------&lt;/span&gt;
 &lt;span class="s"&gt;- Plan&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;                                     &lt;span class="s"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;Node Type&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Index&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Scan"&lt;/span&gt;                 &lt;span class="s"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;Scan Direction&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Backward"&lt;/span&gt;              &lt;span class="s"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;Index Name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index_faults_on_created_at"+&lt;/span&gt;
     &lt;span class="s"&gt;Relation Name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;faults"&lt;/span&gt;                 &lt;span class="s"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;Alias&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;faults"&lt;/span&gt;                         &lt;span class="s"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;Startup Cost&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.29                      +&lt;/span&gt;
     &lt;span class="s"&gt;Total Cost&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5288.04                     +&lt;/span&gt;
     &lt;span class="s"&gt;Plan Rows&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;22265                        +&lt;/span&gt;
     &lt;span class="s"&gt;Plan Width&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1855                        +&lt;/span&gt;
     &lt;span class="s"&gt;Actual Startup Time&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.023              +&lt;/span&gt;
     &lt;span class="s"&gt;Actual Total Time&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;8.778                +&lt;/span&gt;
     &lt;span class="s"&gt;Actual Rows&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;22265                      +&lt;/span&gt;
     &lt;span class="s"&gt;Actual Loops&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1                         +&lt;/span&gt;
   &lt;span class="s"&gt;Triggers&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;                                 &lt;span class="s"&gt;+&lt;/span&gt;
   &lt;span class="s"&gt;Total Runtime&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10.080&lt;/span&gt;
&lt;span class="s"&gt;(1 row)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你要依据多个 columns 排序，你需要创建一个由多个 columns 排序的 index。在 Rails migration 中：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;add_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:faults&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:priority&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:created_at&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;order: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;priority: :asc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;created_at: :desc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当你开始做更复杂的查询时，通过 explain 来执行它们是一个好办法，要趁早并且经常这样。&lt;/p&gt;
&lt;h5 id="　　"&gt;  &lt;/h5&gt;&lt;h2 id="Limits and Offsets"&gt;Limits and Offsets&lt;/h2&gt;
&lt;p&gt;我们多半不会把数据库中的所有数据都放到一个 index 页面里展示。我们用的是 paginate，一次只显示 10, 30 或者 50 条。实现这个的最常规的方法是用 limit 和 offset：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Fault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应的 SQL 为：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;select * from faults limit 10 offset 100;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在如果我们运行 explain，可以看到一些奇怪的东西。扫描的行数是 110，等于 limit 加上 offset。&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# explain (analyze, format yaml) select * from faults limit 10 offset 100;&lt;/span&gt;
              &lt;span class="s"&gt;QUERY PLAN&lt;/span&gt;
&lt;span class="s"&gt;--------------------------------------&lt;/span&gt;
 &lt;span class="s"&gt;- Plan&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;                             &lt;span class="s"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;Node Type&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Limit"&lt;/span&gt;              &lt;span class="s"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;...&lt;/span&gt;
     &lt;span class="s"&gt;Plans&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;                          &lt;span class="s"&gt;+&lt;/span&gt;
       &lt;span class="s"&gt;- Node Type&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Seq&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Scan"&lt;/span&gt;       &lt;span class="s"&gt;+&lt;/span&gt;
         &lt;span class="s"&gt;Actual Rows&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;110            +&lt;/span&gt;
         &lt;span class="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你把 offset 改成 10000 你会看到扫描的行数跳到了 10010，这个查询会变慢 64 倍。&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# explain (analyze, format yaml) select * from faults limit 10 offset 10000;&lt;/span&gt;
              &lt;span class="s"&gt;QUERY PLAN&lt;/span&gt;
&lt;span class="s"&gt;--------------------------------------&lt;/span&gt;
 &lt;span class="s"&gt;- Plan&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;                             &lt;span class="s"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;Node Type&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Limit"&lt;/span&gt;              &lt;span class="s"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;...&lt;/span&gt;
     &lt;span class="s"&gt;Plans&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;                          &lt;span class="s"&gt;+&lt;/span&gt;
       &lt;span class="s"&gt;- Node Type&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Seq&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Scan"&lt;/span&gt;       &lt;span class="s"&gt;+&lt;/span&gt;
         &lt;span class="s"&gt;Actual Rows&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10010          +&lt;/span&gt;
         &lt;span class="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就可以得出一个不好的结论：当分页的时候，后面的页面要比靠前的页面加载得慢。假设一个页面有 100 条记录 (像上面这个示例一样)，那么第 100 页将比第一页慢 13 倍。&lt;/p&gt;

&lt;p&gt;那我们该怎么办？&lt;/p&gt;

&lt;p&gt;老实说，我还没有找到一个完美的解决方案。首先我想的是减小数据量，我就不用开始时分 100 页或者 1000 页了。&lt;/p&gt;

&lt;p&gt;如果你不能减少数据集，最好的方法可能是用&lt;code&gt;where&lt;/code&gt;语句替换掉&lt;code&gt;offset/limit&lt;/code&gt;。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# You could use a date range&lt;/span&gt;
&lt;span class="no"&gt;Fault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"created_at &amp;gt; ? and created_at &amp;lt; ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;days&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ago&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;days&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ago&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# ...or even an id range&lt;/span&gt;
&lt;span class="no"&gt;Fault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"id &amp;gt; ? and id &amp;lt; ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h5 id="　　"&gt;  &lt;/h5&gt;&lt;h2 id="结论"&gt;结论&lt;/h2&gt;
&lt;p&gt;我希望这篇文章能够说服你，真的应该利用好 postgres 的 explain 这个功能来查找你数据库查询中潜在的性能问题。即使是最简单的查询也会导致重大的性能问题，所以这值得去检查。:)&lt;/p&gt;

&lt;p&gt;&lt;a href="http://blog.honeybadger.io/common-rails-idioms-that-kill-database-performance/" rel="nofollow" target="_blank" title=""&gt;原文链接&lt;/a&gt;&lt;/p&gt;</description>
      <author>rennyallen</author>
      <pubDate>Mon, 09 Jan 2017 21:05:14 +0800</pubDate>
      <link>https://ruby-china.org/topics/32085</link>
      <guid>https://ruby-china.org/topics/32085</guid>
    </item>
    <item>
      <title>[译] 简单三步使用计数器缓存</title>
      <description>&lt;p&gt;我最近开始在 Rails 中使用计数器缓存。然而这个上手起来要比指南里写的难一点。&lt;/p&gt;

&lt;p&gt;&lt;a href="http://guides.ruby-china.org/active_record_basics.html" rel="nofollow" target="_blank" title=""&gt;指南&lt;/a&gt;里这样写的：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;缓存关联对象的数量。例如，posts 表中的 comments_count 字段，缓存每篇文章的评论数；&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="http://guides.rubyonrails.org/active_record_basics.html#naming-conventions" rel="nofollow" target="_blank" title=""&gt;英文版&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Used to cache the number of belonging objects on associations. For example, a comments_count column in a Post class that has many instances of Comment will cache the number of existent comments for each post.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;虽然 Rails 内置机制做了困难的这部分，但你仍然需要做一些步骤否则它无法工作。以下是你要让计数器缓存工作需要做的一些事情。&lt;/p&gt;

&lt;p&gt;我们使用和指南里一样的示例：我们想要计算一篇博客文章里的评论数量。&lt;/p&gt;

&lt;p&gt;model 的定义像这样：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:comments&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Comments&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:post&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你需要做的第一件事是加一个 comments_count column 到 Post model 里。你可以用 migration 来做这个。运行了 migration 之后你还需要告诉 rails，这个 column 应该被用来当做计数缓存：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:counter_cache&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;剩下的问题是，如果之前数据库里已经有了 posts 和 comments 那么这个计数器是关闭的。这是因为当你创建或删除 model 的一个实例时计数器缓存增加或减少了。你可以像这样给 posts 重置计数器：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_each&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset_counters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以把这个写成 rake task，那样当你的缓存计数出问题时你就可以运行它。&lt;/p&gt;

&lt;p&gt;&lt;a href="http://yerb.net/blog/2014/03/13/three-easy-steps-to-using-counter-caches-in-rails/" rel="nofollow" target="_blank" title=""&gt;原文&lt;/a&gt;分享出来，欢迎大家交流指正。&lt;/p&gt;</description>
      <author>rennyallen</author>
      <pubDate>Sun, 08 Jan 2017 18:53:36 +0800</pubDate>
      <link>https://ruby-china.org/topics/32073</link>
      <guid>https://ruby-china.org/topics/32073</guid>
    </item>
    <item>
      <title>[译] Terminal 功夫——方便开发者的实用技巧</title>
      <description>&lt;h2 id="前言"&gt;前言&lt;/h2&gt;
&lt;p&gt;这是我在过去几年中提炼出的一些方便你编程的小技巧和提示。在这篇文章中，你将学到如何更有效率地做你每天做的事情。 &lt;/p&gt;
&lt;h6 id="　"&gt; &lt;/h6&gt;&lt;h2 id="快速搜索命令行历史记录"&gt;快速搜索命令行历史记录&lt;/h2&gt;
&lt;p&gt;我真的很难记住一些命令，特别是那些有一大堆参数的。而且有时候更烦，你不得不停下来然后又去 google 你两个小时之前引用的某个命令。不用担心，&lt;code&gt;reverse-i-search&lt;/code&gt;可以拯救你。&lt;strong&gt;打开你的终端，按下&lt;code&gt;Ctrl + R&lt;/code&gt;&lt;/strong&gt;。输入你最近使用的命令的前几个字符，它会显示出符合你要查询的命令。如果有多个匹配的命令，重复按&lt;code&gt;Ctrl + R&lt;/code&gt;直到找到你想要的然后按&lt;code&gt;Ctrl + E&lt;/code&gt;选择即可。&lt;br&gt;
你也可以输入&lt;code&gt;history&lt;/code&gt;就可以显示出你之前执行过的命令列表。&lt;/p&gt;
&lt;h6 id="　"&gt; &lt;/h6&gt;&lt;h2 id="缩短退格时间"&gt;缩短退格时间&lt;/h2&gt;
&lt;p&gt;你是否遇到过很长的查询，比如跨了 4 行，然后你想跳到前边去做一些修改，或者是你写了很长的命令，跨了 4 行，结果发现只是在最开始的地方有点错？这时候你可能要按箭头，然后一个一个字符地把光标移到开头去。其实你可以使用快捷方法，这些方法可以节省你很多按方向键和空格键的时间。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Ctrl + K&lt;/code&gt; - 删除光标右边的所有内容&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Ctrl + W&lt;/code&gt; - 删除光标左边的所有内容（一次一个单词）&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Ctrl + A&lt;/code&gt; - 跳到行首&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Ctrl + E&lt;/code&gt; - 跳到行尾&lt;/p&gt;
&lt;h6 id="　"&gt; &lt;/h6&gt;&lt;h2 id="在 rails console 中用 _ 访问最后一个表达式"&gt;在 rails console 中用 _ 访问最后一个表达式&lt;/h2&gt;
&lt;p&gt;这是另一个我经常犯的错误：在 rails 控制台中查询一个 ActiveRecord model，忘了把结果保存在一个变量里，然后按&lt;code&gt;↑&lt;/code&gt;加上一个变量再做一遍。&lt;/p&gt;

&lt;p&gt;很幸运比我聪明的人也有这个问题，实际上他们已经解决了。每次你在 console 里面执行一个命令或者是功能，其返回值都会被保存在一个叫做&lt;code&gt;_&lt;/code&gt;的变量里（对，就是一个下划线）&lt;/p&gt;

&lt;p&gt;所以，比如你在 console 里面执行了&lt;code&gt;User.first(5)&lt;/code&gt;。你得到了前 5 个 users 但是你忘了存在一个变量里。这时你只需要&lt;code&gt;_.first.name&lt;/code&gt;或者更好的是你可以 &lt;code&gt;users = _&lt;/code&gt;. 现在变量  users 就等于&lt;code&gt;User.first(5)&lt;/code&gt;的返回值了。&lt;/p&gt;
&lt;h6 id="　"&gt; &lt;/h6&gt;&lt;h2 id="找到任何URL的 Controller 和 Action"&gt;找到任何 URL 的 Controller 和 Action&lt;/h2&gt;
&lt;p&gt;这个也超有用，尤其是当你的 routes 文件达到了 500 行，你有 5 个不同的 namespaces，还有一堆自定义的 actions 时。你有个 URL，你想知道它执行的是哪个 Controller 和 Action。常见的方法是打开 routes 文件然后扫描整个文件去查找。或者你会用 &lt;a href="https://github.com/dejan/rails_panel" rel="nofollow" target="_blank" title=""&gt;RailsPanel&lt;/a&gt; 来查看 Controller 和 Action。但这都要耗些时间。&lt;/p&gt;

&lt;p&gt;如果你想要秒秒钟搞定这件事，在 rails console 里面用这个命令：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Rails.application.routes.recognize_path "http://localhost:3000/users/11"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Ps: 当用在不是 GET 的 action 上的时候要小心，可能会返回正确的 controller，错误的 action&lt;/p&gt;
&lt;h6 id="　"&gt; &lt;/h6&gt;&lt;h2 id="在 sandbox 里面玩"&gt;在 sandbox 里面玩&lt;/h2&gt;
&lt;p&gt;你想在 console 中弄乱一些值然后看看程序怎么反应，但是你又不想破坏你的原始数据库？不用害怕，rails console 里面有一个内置的选项为此而设。用以下命令启动 rails console&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rails console --sandbox&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;在这个模式下 console 被包装在数据库事务中启动。当你退出回话后，事务将回滚。所以你可以随意增删改数据，当你退出 console 时数据库会被恢复到原始状态。但要小心在沙箱里面运行事务因为并非所有数据库都可以处理嵌套事务。&lt;/p&gt;
&lt;h6 id="　"&gt; &lt;/h6&gt;&lt;h2 id="重新加载控制台"&gt;重新加载控制台&lt;/h2&gt;
&lt;p&gt;你在 console 里面试了一些东西，不起作用。然后你要改一下你的代码再试一次，这时候不用关掉 console 又重新启动，只需
&lt;code&gt;reload!&lt;/code&gt;&lt;/p&gt;
&lt;h6 id="　"&gt; &lt;/h6&gt;&lt;h2 id="去除ri和rdoc"&gt;去除 ri 和 rdoc&lt;/h2&gt;
&lt;p&gt;&lt;img src="http://www.rubyonrails365.com/images/2015/june/xkcd_bundle_installing-58173d59.png" title="" alt="bundle installing"&gt;&lt;/p&gt;

&lt;p&gt;你在 Github 上看到一个很好的项目，你立即把它 clone 下来然后&lt;code&gt;bundle install&lt;/code&gt;。然后...你花了 30 分钟来下载所有的 gems，而这其中一半的时间都花在了下载 RDoc 和 ri 文件上了，你可能从来不用这些，甚至你可能根本不知道这是什么鬼。可以说只要你联网了你就不是必须要用他们，但是当你 bundle 的时候会自动下载。这里有两个方法来处理：&lt;/p&gt;

&lt;p&gt;第一，如果你是安装单个 gem，使用&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gem install rails --no-ri --no-rdoc&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;这样就告诉 bundler 不要下载 RDoc 和集成 ri 文件。这样就节省了很多时间和空间。&lt;/p&gt;

&lt;p&gt;但上面这个方法只是在你安装单个 gem 的时候有用。如果你想让安装所有 gem 的时候都默认这样做的话：&lt;/p&gt;

&lt;p&gt;步骤 1. 在 home 目录下创建一个文件命名为&lt;code&gt;.gemrc&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cd ~; touch .gemrc;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;步骤 2. 在你的编辑器里面打开这个文件&lt;/p&gt;

&lt;p&gt;&lt;code&gt;subl ~/.gemrc  # 用你的编辑器打开这个文件&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;步骤 3. 将以下代码粘贴进去即可&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gem: --no-ri --no-rdoc
install: --no-rdoc --no-ri
update:  --no-rdoc --no-ri
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们还可以更深入一点。很多大中型项目的 gem 平均要跑 50 秒，这还不包括解决每个 gem 的依赖。例如&lt;a href="https://github.com/discourse/discourse" rel="nofollow" target="_blank" title=""&gt;Discourse&lt;/a&gt;这个项目有大概&lt;a href="https://github.com/discourse/discourse/blob/master/Gemfile" rel="nofollow" target="_blank" title=""&gt;150 个 gems&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;我们可以通过并发安装 gem 让这变得更快，确保充分利用你的网络带宽处理能力。我们通过给 bundler 加上&lt;code&gt;--jobs&lt;/code&gt;这个参数来完成。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bundle install discourse --jobs=4&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;运行这个命令你会看到 gems 的安装快得飞起。然而这不是任何时候都适用的，有时候会出现死锁或者冲突。解决方法只需运行：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bundle install discourse --jobs=1&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;这样就设置回了默认的情况。&lt;/p&gt;
&lt;h6 id="　"&gt; &lt;/h6&gt;&lt;h2 id="看一下gem内部"&gt;看一下 gem 内部&lt;/h2&gt;
&lt;p&gt;或许因为你的好奇心，你想看一下一个特定的功能是怎么执行的，想看一下你正在使用的一个 gem 的源代码。是的，你可以每次去 google github 里面的项目然后在那里看。但是其实可以不用这样。只需要&lt;/p&gt;

&lt;p&gt;&lt;code&gt;EDITOR=subl bundle open devise&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;subl&lt;/code&gt;这里是你的编辑器，&lt;code&gt;devise&lt;/code&gt;是你的 gem 名字。这里我们打开了 devise 这个 gem。这样做的好处是它可以打开你正在使用的 gem 的版本。&lt;/p&gt;

&lt;p&gt;然后你可以在项目里面做你想做的事情，你可以修改代码来理解它做了什么事，可以修改下 function 甚至是增加 functions。当你看完了代码之后，你可以用以下命令使它回到初始状态&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gem pristine devise&lt;/code&gt;&lt;/p&gt;
&lt;h6 id="　"&gt; &lt;/h6&gt;&lt;h2 id="在你的代码里面写注释"&gt;在你的代码里面写注释&lt;/h2&gt;
&lt;p&gt;当你写代码的时候，你可能想在代码里面快速地做一些笔记。或许你可能想写下“想要重构”之类的。不用切换到其他地方去做笔记，rails 提供了一个东西，你只需要在你的程序里面使用 &lt;code&gt;#TODO&lt;/code&gt;. &lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;#TODO make this a one line function.&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后你可以在 console 里面输入以下命令来查看你的所有笔记：
&lt;code&gt;bundle exec rake notes:todo&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;但这样久了之后你可能会发现你有很多 todos，你可能想要描述得更加具体，你可以做一些自定义的修改，比如：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;#REFACTOR make this a one line function.&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后这样去查找：
&lt;code&gt;bundle exec rake notes:custom ANNOTATION=REFACTOR&lt;/code&gt;&lt;/p&gt;
&lt;h6 id="　"&gt; &lt;/h6&gt;&lt;h2 id="找到任何方法的源地址"&gt;找到任何方法的源地址&lt;/h2&gt;
&lt;p&gt;有时你在 console 中使用一个方法，尤其是你引用的 gem 包里面定义的方法，你想看看它的源代码。方法经常在不同的文件之间有相同的名称，你不确定是调用的哪个，或者说想查看这个方法在哪里定义的。这时候找到源代码的位置的最简单方法是：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user.method(:password=).source_location 
=&amp;gt; #["/Users/ror365/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/activemodel-4.2.0/lib/active_model/secure_password.rb", 119]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回的是确切的文件路径和相应的行号，然后你就可以按照这个去查找啦。&lt;/p&gt;
&lt;h6 id="　"&gt; &lt;/h6&gt;&lt;h2 id="列出所有的 rake 任务"&gt;列出所有的 rake 任务&lt;/h2&gt;
&lt;p&gt;如果你有一个较大的应用程序，你写了多个 rake 任务，然后 rails 本身也内置了一些 rake 任务，这时候你可能很难记住或者找到他们。最简单快速的方法来找到 rake 任务，运行：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bundle exec rake -T&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;-T&lt;/code&gt;这个参数告诉 rake 列出在这个应用程序里面找到的所有 rake 任务，而且可以打印出这个任务的描述。&lt;/p&gt;
&lt;h6 id="　"&gt; &lt;/h6&gt;&lt;h2 id="清理一些磁盘空间"&gt;清理一些磁盘空间&lt;/h2&gt;
&lt;p&gt;我的一个同事，他的 Macbook Pro 有 120 个 G 的硬盘。时间长了就占满了，然后他想找一些东西来删掉。我碰巧有次看到他在做这个事，然后就问他：“你清理你的 rails logs 了吗？”他说没有，然后他去看 log，他本地的 development log 已经占用了 2G 的空间。看来从他买了这电脑就一直没清理过。&lt;/p&gt;

&lt;p&gt;日复一日地开发一个应用程序，你可能没注意到你所做的每个请求都写在了开发日志 (development log) 里面。这可能并不是很大，但是它会快速地增加。不用手动去删除它，你可以运行这个专门为此设计的命令：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bundle exec rake log:clear&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;这会清理你的日志从而你可以获得一些硬盘空间。&lt;/p&gt;
&lt;h6 id="　"&gt; &lt;/h6&gt;&lt;h2 id="找出所有过时的gems"&gt;找出所有过时的 gems&lt;/h2&gt;
&lt;p&gt;开源世界，变化万千。如果你有一堆 gems，你可能经常需要更新他们。然而运行&lt;code&gt;bundle update&lt;/code&gt;命令有点危险，因为它会破坏一些东西。我建议在你的项目里先执行&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bundle outdated&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;这样会列出所有需要更新的 gems。然后你可以根据这个去 bundle update 某个单独的 gem.&lt;/p&gt;
&lt;h6 id="　"&gt; &lt;/h6&gt;&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;一个人可以知道的小技巧是无限的，知道这些可能并不会让你成为一个更好的开发者，而且一个知道这些小提示和技巧的人也不见得比你厉害，虽然往往会有这样的错觉。这些小技巧可以使你更高效一点，没有别的。&lt;/p&gt;

&lt;p&gt;（&lt;a href="http://www.rubyonrails365.com/tips-and-tricks/" rel="nofollow" target="_blank" title=""&gt;原文&lt;/a&gt;翻译时有改动）&lt;/p&gt;
&lt;h6 id="　"&gt; &lt;/h6&gt;</description>
      <author>rennyallen</author>
      <pubDate>Fri, 02 Dec 2016 15:42:28 +0800</pubDate>
      <link>https://ruby-china.org/topics/31767</link>
      <guid>https://ruby-china.org/topics/31767</guid>
    </item>
  </channel>
</rss>
