Benito SernaTips and articles for Ruby on Rails developershttp://bhserna.com/2024-02-09T23:33:00ZBenito SernaSimple Searchable module for searching with Rails and SQLite's LIKE/simple-searchable-module-for-searching-with-rails-and-sqlite-like.html2024-02-09T23:33:00Z2024-02-09T23:33:00ZBenito Serna<p>Yesterday, I found myself <a href="https://twitter.com/bhserna/status/1755371440633774261">struggling</a> with <a href="https://github.com/oldmoe/litestack/wiki/Litesearch-ActiveRecord-guide">“litesearch”</a>, and in my frustration, I decided to create a simple “Searchable” module to perform a “LIKE” search but with a cleaner interface/API.</p>
<p>If you’re looking for a straightforward way to implement a search feature in your application using “LIKE”, this post might be helpful to you.</p>
<p>…However, it turns out <strong>the issue wasn’t with “litesearch” at all</strong>. <a href="https://twitter.com/bhserna/status/1755749698017648649">It was actually related to the parallelization of the specs</a>. For now, I’ll continue using “litesearch” instead of the code provided in this post. Nevertheless, I wanted to share it because it might be useful to you (or even to myself in the future).</p>
<h2>The Searchable Module</h2>
<p>You can include the following code in <code>app/models/concerns/searchable.rb</code>:</p>
<pre><code class="highlight ruby"><span class="k">module</span> <span class="nn">Searchable</span>
<span class="kp">extend</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Concern</span>
<span class="n">included</span> <span class="k">do</span>
<span class="n">cattr_accessor</span> <span class="p">:</span><span class="n">searchable_fields</span><span class="p">,</span> <span class="ss">:searchable_associations_fields</span>
<span class="k">end</span>
<span class="n">class_methods</span> <span class="k">do</span>
<span class="k">def</span> <span class="nf">search_on</span><span class="p">(</span><span class="o">*</span><span class="n">searchable_fields</span><span class="p">,</span> <span class="o">**</span><span class="n">searchable_associations_fields</span><span class="p">)</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">searchable_fields</span> <span class="o">=</span> <span class="n">searchable_fields</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">searchable_associations_fields</span> <span class="o">=</span> <span class="n">searchable_associations_fields</span>
<span class="n">scope</span> <span class="p">:</span><span class="n">search</span><span class="p">,</span> <span class="o">-></span><span class="p">(</span><span class="n">query</span><span class="p">)</span> <span class="k">do</span>
<span class="n">joins</span><span class="p">(</span><span class="n">searchable_joins</span><span class="p">).</span><span class="nf">where</span><span class="p">(</span><span class="n">searchable_where_clause</span><span class="p">,</span> <span class="ss">query: </span><span class="s2">"%</span><span class="si">#{</span><span class="n">query</span><span class="si">}</span><span class="s2">%"</span><span class="p">)</span>
<span class="k">end</span>
<span class="n">scope</span> <span class="p">:</span><span class="n">maybe_search</span><span class="p">,</span> <span class="o">-></span><span class="p">(</span><span class="n">query</span><span class="p">)</span> <span class="k">do</span>
<span class="n">search</span><span class="p">(</span><span class="n">query</span><span class="p">)</span> <span class="k">if</span> <span class="n">query</span><span class="p">.</span><span class="nf">present?</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">searchable_joins</span>
<span class="n">searchable_associations_fields</span><span class="p">.</span><span class="nf">keys</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">searchable_where_clause</span>
<span class="n">all_normalized_searchable_fields</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">field</span><span class="o">|</span> <span class="s2">"</span><span class="si">#{</span><span class="n">field</span><span class="si">}</span><span class="s2"> LIKE :query"</span> <span class="p">}.</span><span class="nf">join</span><span class="p">(</span><span class="s2">" OR "</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">all_normalized_searchable_fields</span>
<span class="n">normalized_searchable_fields</span> <span class="o">+</span> <span class="n">normalized_searchable_associations_fields</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">normalized_searchable_fields</span>
<span class="n">searchable_fields</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">field</span><span class="o">|</span> <span class="p">[</span><span class="n">table_name</span><span class="p">,</span> <span class="n">field</span><span class="p">].</span><span class="nf">join</span><span class="p">(</span><span class="s2">"."</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">normalized_searchable_associations_fields</span>
<span class="n">searchable_associations_fields</span><span class="p">.</span><span class="nf">flat_map</span> <span class="k">do</span> <span class="o">|</span><span class="n">association</span><span class="p">,</span> <span class="n">fields</span><span class="o">|</span>
<span class="n">table_name</span> <span class="o">=</span> <span class="n">reflect_on_association</span><span class="p">(</span><span class="n">association</span><span class="p">).</span><span class="nf">klass</span><span class="p">.</span><span class="nf">table_name</span>
<span class="no">Array</span><span class="p">.</span><span class="nf">wrap</span><span class="p">(</span><span class="n">fields</span><span class="p">).</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">field</span><span class="o">|</span> <span class="p">[</span><span class="n">table_name</span><span class="p">,</span> <span class="n">field</span><span class="p">].</span><span class="nf">join</span><span class="p">(</span><span class="s2">"."</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<h2>How to Use It</h2>
<p>You can use the module in your models like this:</p>
<pre><code class="highlight ruby"><span class="k">class</span> <span class="nc">Contact</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="kp">include</span> <span class="no">Searchable</span>
<span class="n">search_on</span> <span class="p">:</span><span class="nb">name</span><span class="p">,</span> <span class="ss">:last_name</span><span class="p">,</span> <span class="ss">:full_name</span><span class="p">,</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">:current_company</span><span class="p">,</span> <span class="ss">:phone</span>
<span class="k">end</span>
</code></pre>
<p>You can also search on associations using a hash key with the name of the association and a list of attributes to search on.</p>
<p>For example:</p>
<pre><code class="highlight ruby"><span class="k">class</span> <span class="nc">QuoteRequest</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="kp">include</span> <span class="no">Searchable</span>
<span class="n">belongs_to</span> <span class="p">:</span><span class="n">requester</span><span class="p">,</span> <span class="ss">class_name: </span><span class="s2">"Contact"</span>
<span class="n">search_on</span> <span class="p">:</span><span class="n">custom_id</span><span class="p">,</span> <span class="ss">:brand_name</span><span class="p">,</span> <span class="ss">:product_name</span><span class="p">,</span> <span class="ss">:more_info</span><span class="p">,</span> <span class="ss">:quantity</span><span class="p">,</span> <span class="ss">requester: :full_name</span>
<span class="k">end</span>
</code></pre>
<p>In this example, you’ll also search on the <code>requester.full_name</code> attribute, which corresponds to the <code>"contacts.full_name"</code> column.</p>
<p>If you want to search on multiple fields of the “requester”, you can do something like this:</p>
<pre><code class="highlight ruby"><span class="k">class</span> <span class="nc">QuoteRequest</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="kp">include</span> <span class="no">Searchable</span>
<span class="n">belongs_to</span> <span class="p">:</span><span class="n">requester</span><span class="p">,</span> <span class="ss">class_name: </span><span class="s2">"Contact"</span>
<span class="n">search_on</span> <span class="p">:</span><span class="n">custom_id</span><span class="p">,</span> <span class="ss">:brand_name</span><span class="p">,</span> <span class="ss">:product_name</span><span class="p">,</span> <span class="ss">:more_info</span><span class="p">,</span> <span class="ss">:quantity</span><span class="p">,</span> <span class="ss">requester: </span><span class="p">[</span><span class="ss">:email</span><span class="p">,</span> <span class="ss">:full_name</span><span class="p">]</span>
<span class="k">end</span>
</code></pre>
<h2>The tests to show how each part works</h2>
<p>I didn’t write a lot of tests for it, and I didn’t write them in a generic way. However, I will show them to you because they can help you understand the purpose of each part of the code.</p>
<pre><code class="highlight ruby"><span class="nb">require</span> <span class="s2">"test_helper"</span>
<span class="k">class</span> <span class="nc">SearchableTest</span> <span class="o"><</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TestCase</span>
<span class="nb">test</span> <span class="s2">"quote_requests metadata"</span> <span class="k">do</span>
<span class="c1"># Keeps the list of fields to search on the record</span>
<span class="n">assert_equal</span> <span class="no">QuoteRequest</span><span class="p">.</span><span class="nf">searchable_fields</span><span class="p">,</span> <span class="p">[</span><span class="ss">:custom_id</span><span class="p">,</span> <span class="ss">:brand_name</span><span class="p">,</span> <span class="ss">:product_name</span><span class="p">,</span> <span class="ss">:more_info</span><span class="p">,</span> <span class="ss">:quantity</span><span class="p">]</span>
<span class="c1"># Keeps a hash with the list of fields to search for each association</span>
<span class="n">assert_equal</span> <span class="no">QuoteRequest</span><span class="p">.</span><span class="nf">searchable_associations_fields</span><span class="p">,</span> <span class="p">{</span><span class="ss">requester: :full_name</span><span class="p">}</span>
<span class="c1"># Builds the list of fields with the table_name, to be used later in the query</span>
<span class="n">assert_equal</span> <span class="no">QuoteRequest</span><span class="p">.</span><span class="nf">normalized_searchable_fields</span><span class="p">,</span> <span class="p">[</span><span class="s2">"quote_requests.custom_id"</span><span class="p">,</span> <span class="s2">"quote_requests.brand_name"</span><span class="p">,</span> <span class="s2">"quote_requests.product_name"</span><span class="p">,</span> <span class="s2">"quote_requests.more_info"</span><span class="p">,</span> <span class="s2">"quote_requests.quantity"</span><span class="p">]</span>
<span class="c1"># Builds the list of fields from the associations with the table_name, to be used later in the query</span>
<span class="n">assert_equal</span> <span class="no">QuoteRequest</span><span class="p">.</span><span class="nf">normalized_searchable_associations_fields</span><span class="p">,</span> <span class="p">[</span><span class="s2">"contacts.full_name"</span><span class="p">]</span>
<span class="c1"># The string to be used in the where clause</span>
<span class="n">assert_equal</span> <span class="no">QuoteRequest</span><span class="p">.</span><span class="nf">searchable_where_clause</span><span class="p">,</span> <span class="s2">"quote_requests.custom_id LIKE :query OR quote_requests.brand_name LIKE :query OR quote_requests.product_name LIKE :query OR quote_requests.more_info LIKE :query OR quote_requests.quantity LIKE :query OR contacts.full_name LIKE :query"</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"contacts example"</span> <span class="k">do</span>
<span class="n">result</span> <span class="o">=</span> <span class="no">Contact</span><span class="p">.</span><span class="nf">search</span><span class="p">(</span><span class="s2">"Pedro"</span><span class="p">)</span>
<span class="n">assert_equal</span> <span class="n">result</span><span class="p">.</span><span class="nf">count</span><span class="p">,</span> <span class="mi">1</span>
<span class="n">assert_equal</span> <span class="n">result</span><span class="p">.</span><span class="nf">first</span><span class="p">,</span> <span class="n">contacts</span><span class="p">(</span><span class="ss">:one</span><span class="p">)</span>
<span class="n">result</span> <span class="o">=</span> <span class="no">Contact</span><span class="p">.</span><span class="nf">search</span><span class="p">(</span><span class="s2">"Perez"</span><span class="p">)</span>
<span class="n">assert_equal</span> <span class="n">result</span><span class="p">.</span><span class="nf">count</span><span class="p">,</span> <span class="mi">1</span>
<span class="n">assert_equal</span> <span class="n">result</span><span class="p">.</span><span class="nf">first</span><span class="p">,</span> <span class="n">contacts</span><span class="p">(</span><span class="ss">:two</span><span class="p">)</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"quote_requests example"</span> <span class="k">do</span>
<span class="n">result</span> <span class="o">=</span> <span class="no">QuoteRequest</span><span class="p">.</span><span class="nf">search</span><span class="p">(</span><span class="s2">"Pedro"</span><span class="p">)</span>
<span class="n">assert_equal</span> <span class="mi">1</span><span class="p">,</span> <span class="n">result</span><span class="p">.</span><span class="nf">count</span>
<span class="n">assert_equal</span> <span class="n">quote_requests</span><span class="p">(</span><span class="ss">:one</span><span class="p">),</span> <span class="n">result</span><span class="p">.</span><span class="nf">first</span>
<span class="n">result</span> <span class="o">=</span> <span class="no">QuoteRequest</span><span class="p">.</span><span class="nf">search</span><span class="p">(</span><span class="s2">"Perez"</span><span class="p">)</span>
<span class="n">assert_equal</span> <span class="mi">1</span><span class="p">,</span> <span class="n">result</span><span class="p">.</span><span class="nf">count</span>
<span class="n">assert_equal</span> <span class="n">quote_requests</span><span class="p">(</span><span class="ss">:two</span><span class="p">),</span> <span class="n">result</span><span class="p">.</span><span class="nf">first</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<h2>Maybe you can use it</h2>
<p>While I won’t be using this module for now due to my resolution of the issue with “litesearch”, perhaps you may find it useful.</p>
<p>Whether you’re interested in adapting it for PostgreSQL or exploring alternative search solutions, I hope this code may serve as a helpful reference or source of inspiration.</p>
A "read-more" behavior truncating by number of lines with css line-clamp and Stimulus.js/read-more-line-clamp-stimulus.html2021-04-03T00:28:00Z2021-04-03T00:28:00ZBenito Serna<p>I have already shared <a href="truncating-multiple-line-text-read-more.html">a way of implementing a “read-more” behavior truncating by the number of lines instead of the number of words</a>.</p>
<p>But now I want to share how you can do it using the <code>line-clamp</code> css property.</p>
<p></p>
<h2 id="example">The example</h2>
<iframe class="w-100 ba b--light-blue" style="height: 300px" src="read-more-line-clamp-stimulus/example.html"></iframe>
<p><a href="read-more-line-clamp-stimulus/example.html">Visit example page</a></p>
<h2 id="html_and_css">The html and css</h2>
<p>Your are going to need…</p>
<ul>
<li>A <code>linesValue</code> to configure the number of lines that we want to display,</li>
<li>A class to <code>hide</code> the buttons and a class to <code>truncate</code> the content…</li>
<li>3 targets, the <code>content</code>, the <code>moreButton</code> and the <code>lessButton</code>…</li>
<li>3 actions
<ul>
<li><code>resize@window->read-more#render</code> - to calculate the truncation on rezise.</li>
<li><code>click->read-more#showMore</code></li>
<li><code>click->read-more#showLess</code></li>
</ul></li>
</ul>
<pre><code class="highlight css"><span class="nt">html</span> <span class="p">{</span> <span class="nl">line-height</span><span class="p">:</span> <span class="m">1.5</span> <span class="p">}</span>
<span class="nc">.hide</span> <span class="p">{</span> <span class="nl">display</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span> <span class="p">}</span>
<span class="nc">.line-clamp</span> <span class="p">{</span>
<span class="nl">overflow</span> <span class="p">:</span> <span class="nb">hidden</span><span class="p">;</span>
<span class="nl">text-overflow</span><span class="p">:</span> <span class="n">ellipsis</span><span class="p">;</span>
<span class="nl">display</span><span class="p">:</span> <span class="n">-webkit-box</span><span class="p">;</span>
<span class="nl">-webkit-box-orient</span><span class="p">:</span> <span class="n">vertical</span><span class="p">;</span>
<span class="p">}</span>
</code></pre>
<pre><code class="highlight erb"><span class="nt"><div</span> <span class="na">data-controller=</span><span class="s">"read-more"</span>
<span class="na">data-read-more-lines-value=</span><span class="s">"3"</span>
<span class="na">data-read-more-hide-class=</span><span class="s">"hide"</span>
<span class="na">data-read-more-truncate-class=</span><span class="s">"line-clamp"</span>
<span class="na">data-action=</span><span class="s">"resize@window->read-more#render"</span><span class="nt">></span>
<span class="nt"><p</span> <span class="na">data-read-more-target=</span><span class="s">"content"</span><span class="nt">></span>
The content that you want to truncate...
<span class="nt"></p></span>
<span class="nt"><button</span> <span class="na">class=</span><span class="s">"hide"</span>
<span class="na">data-read-more-target=</span><span class="s">"moreButton"</span>
<span class="na">data-action=</span><span class="s">"read-more#showMore"</span><span class="nt">></span>
Show more
<span class="nt"></button></span>
<span class="nt"><button</span> <span class="na">class=</span><span class="s">"hide"</span>
<span class="na">data-read-more-target=</span><span class="s">"lessButton"</span>
<span class="na">data-action=</span><span class="s">"read-more#showLess"</span><span class="nt">></span>
Show less
<span class="nt"></button></span>
<span class="nt"></div></span>
</code></pre>
<h2 id="javascript">The javascript</h2>
<p>You are going to use <code>line-clamp</code> to truncate the content, assigning the configured <code>linesValue</code> to the property <code>-webkit-line-clamp</code>.</p>
<p>But you are going to do it just if the <code>height</code> is greater that the <code>expectedHeigt</code> that is the product of the <code>linesValue</code> with the <code>lineHeight</code>.</p>
<pre><code class="highlight javascript"><span class="kr">import</span> <span class="p">{</span> <span class="nx">Controller</span> <span class="p">}</span> <span class="nx">from</span> <span class="s2">"@hotwired/stimulus"</span><span class="p">;</span>
<span class="kr">export</span> <span class="k">default</span> <span class="kr">class</span> <span class="kr">extends</span> <span class="nx">Controller</span> <span class="p">{</span>
<span class="kr">static</span> <span class="nx">targets</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"content"</span><span class="p">,</span> <span class="s2">"moreButton"</span><span class="p">,</span> <span class="s2">"lessButton"</span><span class="p">];</span>
<span class="kr">static</span> <span class="nx">classes</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"truncate"</span><span class="p">,</span> <span class="s2">"hide"</span><span class="p">]</span>
<span class="kr">static</span> <span class="nx">values</span> <span class="o">=</span> <span class="p">{</span> <span class="na">lines</span><span class="p">:</span> <span class="nb">Number</span> <span class="p">}</span>
<span class="nx">connect</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">render</span><span class="p">()</span>
<span class="p">}</span>
<span class="nx">render</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">showAllContent</span><span class="p">()</span>
<span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">height</span><span class="p">()</span> <span class="o">></span> <span class="k">this</span><span class="p">.</span><span class="nx">expectedHeight</span><span class="p">())</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">showLess</span><span class="p">()</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">showAllContent</span><span class="p">()</span>
<span class="k">this</span><span class="p">.</span><span class="nx">hide</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">moreButtonTarget</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">hide</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">lessButtonTarget</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nx">showMore</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">showAllContent</span><span class="p">();</span>
<span class="k">this</span><span class="p">.</span><span class="nx">hide</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">moreButtonTarget</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">show</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">lessButtonTarget</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">showLess</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">truncateContent</span><span class="p">();</span>
<span class="k">this</span><span class="p">.</span><span class="nx">hide</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">lessButtonTarget</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">show</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">moreButtonTarget</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">showAllContent</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">contentTarget</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">remove</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">truncateClass</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">truncateContent</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">contentTarget</span><span class="p">.</span><span class="nx">style</span><span class="p">[</span><span class="s2">"-webkit-line-clamp"</span><span class="p">]</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">linesValue</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="nx">contentTarget</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">truncateClass</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">show</span><span class="p">(</span><span class="nx">target</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">target</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">remove</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">hideClass</span><span class="p">)</span>
<span class="p">}</span>
<span class="nx">hide</span><span class="p">(</span><span class="nx">target</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">target</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">hideClass</span><span class="p">)</span>
<span class="p">}</span>
<span class="nx">lineHeight</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">style</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">getComputedStyle</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">contentTarget</span><span class="p">)</span>
<span class="k">return</span> <span class="nb">parseFloat</span><span class="p">(</span><span class="nx">style</span><span class="p">.</span><span class="nx">lineHeight</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">height</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">contentTarget</span><span class="p">.</span><span class="nx">offsetHeight</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">expectedHeight</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">linesValue</span> <span class="o">*</span> <span class="k">this</span><span class="p">.</span><span class="nx">lineHeight</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<h2>Other ways to do it…</h2>
<p>If for some reason, you can’t use <code>line-clamp</code>, maybe you can try doing the <a href="truncating-multiple-line-text-read-more.html">truncation with custom javascript</a>.</p>
<p>And if you can truncate by number of characters, you can check <a href="read-more-rails-truncate-stimulus.html">this example using the “truncate” helper.</a></p>
Copy shoes.rb and use stacks and flows to build layouts/copy-shoes-rb-and-use-stacks-and-flows-to-build-layouts.html2024-01-24T00:07:00Z2024-01-24T00:07:00ZBenito Serna<p>I was reading about <a href="http://shoesrb.com/">shoes.rb</a> and, while scanning the book and walkthrough, I discovered a pattern that caught my attention for its power and simplicity.</p>
<p>The combination of “stacks” and “flows” allows you to build complex layouts, like the following example in Ruby:</p>
<pre><code class="highlight ruby"><span class="no">Shoes</span><span class="p">.</span><span class="nf">app</span> <span class="k">do</span>
<span class="n">background</span> <span class="s2">"#EFC"</span>
<span class="n">border</span><span class="p">(</span><span class="s2">"#BE8"</span><span class="p">,</span> <span class="ss">strokewidth: </span><span class="mi">6</span><span class="p">)</span>
<span class="n">stack</span><span class="p">(</span><span class="ss">margin: </span><span class="mi">12</span><span class="p">)</span> <span class="k">do</span>
<span class="n">para</span> <span class="s2">"Enter your name"</span>
<span class="n">flow</span> <span class="k">do</span>
<span class="n">edit_line</span>
<span class="n">button</span> <span class="s2">"OK"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<h2>What are a stacks and a flows?</h2>
<ol>
<li>A <strong>stack</strong> places one thing on top of another, similar to blocks in HTML.</li>
<li>A <strong>flow</strong> arranges items next to each other on a horizontal line, like inline blocks in HTML.</li>
</ol>
<p>With CSS, you can implement them like this:</p>
<pre><code class="highlight css"><span class="nc">.stack</span> <span class="p">{</span>
<span class="nl">display</span><span class="p">:</span> <span class="n">flex</span><span class="p">;</span>
<span class="py">gap</span><span class="p">:</span> <span class="m">1em</span><span class="p">;</span>
<span class="nl">flex-direction</span><span class="p">:</span> <span class="n">column</span><span class="p">;</span>
<span class="nl">justify-content</span><span class="p">:</span> <span class="n">flex-start</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.flow</span> <span class="p">{</span>
<span class="nl">display</span><span class="p">:</span> <span class="n">flex</span><span class="p">;</span>
<span class="py">gap</span><span class="p">:</span> <span class="m">0.5em</span><span class="p">;</span>
<span class="nl">align-items</span><span class="p">:</span> <span class="nb">baseline</span><span class="p">;</span>
<span class="nl">flex-wrap</span><span class="p">:</span> <span class="n">wrap</span><span class="p">;</span>
<span class="p">}</span>
</code></pre>
<p>Here’s a pen with some examples from the walkthrough to help you visualize how you can combine these two CSS rules:</p>
<p><details>
<summary>View CodePen Example</summary>
<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="NWJbvrW" data-user="bhserna" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
<span>See the Pen <a href="https://codepen.io/bhserna/pen/NWJbvrW">
Untitled</a> by Benito Serna (<a href="https://codepen.io/bhserna">@bhserna</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>
</details></p>
<h2>Personal testing of the idea</h2>
<p>I have been using a slightly modified version of these CSS classes in a personal project where I am also using <a href="https://open-props.style/">open-props</a>.</p>
<p>These are the CSS classes that I am using:</p>
<pre><code class="highlight css"><span class="nc">.stack</span> <span class="p">{</span>
<span class="nl">display</span><span class="p">:</span> <span class="n">flex</span><span class="p">;</span>
<span class="py">gap</span><span class="p">:</span> <span class="n">var</span><span class="p">(</span><span class="n">--size-3</span><span class="p">);</span>
<span class="nl">flex-direction</span><span class="p">:</span> <span class="n">column</span><span class="p">;</span>
<span class="nl">justify-content</span><span class="p">:</span> <span class="n">flex-start</span><span class="p">;</span>
<span class="err">&.sm</span> <span class="err">{</span>
<span class="py">gap</span><span class="p">:</span> <span class="n">var</span><span class="p">(</span><span class="n">--size-1</span><span class="p">);</span>
<span class="p">}</span>
<span class="o">&</span><span class="nc">.lg</span> <span class="p">{</span>
<span class="py">gap</span><span class="p">:</span> <span class="n">var</span><span class="p">(</span><span class="n">--size-7</span><span class="p">);</span>
<span class="p">}</span>
<span class="err">}</span>
<span class="nc">.flow</span> <span class="p">{</span>
<span class="nl">display</span><span class="p">:</span> <span class="n">flex</span><span class="p">;</span>
<span class="py">gap</span><span class="p">:</span> <span class="n">var</span><span class="p">(</span><span class="n">--size-2</span><span class="p">);</span>
<span class="nl">align-items</span><span class="p">:</span> <span class="nb">baseline</span><span class="p">;</span>
<span class="nl">flex-wrap</span><span class="p">:</span> <span class="n">wrap</span><span class="p">;</span>
<span class="err">&.centered</span> <span class="err">{</span>
<span class="nl">align-items</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
<span class="p">}</span>
<span class="o">&</span><span class="nc">.justify-between</span> <span class="p">{</span>
<span class="nl">justify-content</span><span class="p">:</span> <span class="n">space-between</span><span class="p">;</span>
<span class="p">}</span>
<span class="o">&</span><span class="nc">.sm</span> <span class="p">{</span>
<span class="py">gap</span><span class="p">:</span> <span class="n">var</span><span class="p">(</span><span class="n">--size-1</span><span class="p">);</span>
<span class="p">}</span>
<span class="o">&</span><span class="nc">.lg</span> <span class="p">{</span>
<span class="py">gap</span><span class="p">:</span> <span class="n">var</span><span class="p">(</span><span class="n">--size-3</span><span class="p">);</span>
<span class="p">}</span>
<span class="err">}</span>
</code></pre>
<p>I don’t have enough experience with them to be sure that you should adopt these classes in all your projects, but I think you should at least test them in your personal projects and play around with them.</p>
<p>If you use something like Tailwind CSS, maybe you can write a partial or component to introduce this abstraction.</p>
<h2>Maybe this names can break your UI kit</h2>
<p>The names “stack” and “flow” are used as UI elements by some HTML or native UI kits.</p>
<p>So, if you are testing them, check if these classes are not already used in your project.</p>
Fix n+1 queries by caching computed values/fix-n+1-queries-by-caching-computed-values.html2023-07-19T23:19:00Z2023-07-19T23:19:00ZBenito Serna<p>N+1 queries are not always a problem, but I have seen that most of the n+1 queries that are really a problem are when we need to fetch data to compute something.</p>
<p>Here I will try to share some examples of posible expensive computations candidates to be cached and some patterns that you could use to save different kind of values.</p>
<h2>Some examples of possible expensive computations</h2>
<p>I think the most common computation in many apps will be a count. It is that common that rails already have “counter caches”.</p>
<p>But sometimes you will need to save counts where a counter cache won’t be enough, like:</p>
<ul>
<li>Counts for “has many through” associations.</li>
<li>Counts for a scope of the association, like just the “positive reactions”, or the “completed orders”.</li>
</ul>
<p>Or maybe other examples where you need other type of aggregation like:</p>
<ul>
<li>The balance in an account</li>
<li>The a profile completeness percentage</li>
<li>A TIR, current value of a portafolio, or other financial calculations</li>
</ul>
<h2>Should you always cache computations?</h2>
<p>No, not really, you should evaluate the pros and cons in each situation.</p>
<p>Sometimes you will need a way to cache the value since the first implementation, but other times it will be better to wait and learn a little more about the problem and cache the value until is really necessary.</p>
<p>Sometimes pre-compute the value will be very easy, but sometimes could be more expensive or very hard, or maybe just not needed.</p>
<h2>Patterns to save computed values</h2>
<p>There are many ways to save a value, but here I will show you three patterns that I use a lot.</p>
<h3>Using a before_save callback</h3>
<p>You could use it for calculations that involve information on the same record, like a profile completeness calculation.</p>
<pre><code class="highlight ruby"><span class="k">class</span> <span class="nc">Profile</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">before_save</span> <span class="p">:</span><span class="n">set_completeness_percentage</span>
<span class="k">def</span> <span class="nf">set_completeness_percentage</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">completeness_percentage</span> <span class="o">=</span> <span class="n">calculate_completeness_percentage</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<p>When you use a <code>before_save</code> callback, you don’t need to <a href="https://guides.rubyonrails.org/active_record_callbacks.html#available-callbacks">save the record</a>, you just need to change the value and the record will be saved including the fields you modified.</p>
<h3>Using after_touch callback</h3>
<p>You could use it when you need to save or change a value for things that happen in other records, like updating the balance when an entry is created or updated:</p>
<pre><code class="highlight ruby"><span class="k">class</span> <span class="nc">Account</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">has_many</span> <span class="p">:</span><span class="n">entries</span>
<span class="n">after_touch</span> <span class="p">:</span><span class="n">update_balance</span>
<span class="k">def</span> <span class="nf">update_balance</span>
<span class="n">update</span><span class="p">(</span><span class="ss">balance: </span><span class="n">entries</span><span class="p">.</span><span class="nf">sum</span><span class="p">(</span><span class="ss">:amount</span><span class="p">))</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">class</span> <span class="nc">Entry</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">belongs_to</span> <span class="p">:</span><span class="n">account</span><span class="p">,</span> <span class="ss">touch: </span><span class="kp">true</span>
<span class="k">end</span>
</code></pre>
<h2>Using after_commit + background job</h2>
<p>Use it for calculations are expensive to run on the same process and is better to run them asynchronously.</p>
<pre><code class="highlight ruby"><span class="k">class</span> <span class="nc">Profile</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">after_commit</span> <span class="p">:</span><span class="n">set_completeness_percentage_later</span>
<span class="k">def</span> <span class="nf">set_completeness_percentage_later</span>
<span class="no">Profile</span><span class="o">::</span><span class="no">SetCompletenessPercentageProfileJob</span><span class="p">.</span><span class="nf">perform_later</span><span class="p">(</span><span class="nb">self</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">set_completeness_percentage</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">completeness_percentage</span> <span class="o">=</span> <span class="n">calculate_completeness_percentage</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">class</span> <span class="nc">Profile</span><span class="o">::</span><span class="no">SetCompletenessPercentageProfileJob</span> <span class="o"><</span> <span class="no">ApplicationJob</span>
<span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">profile</span><span class="p">)</span>
<span class="n">profile</span><span class="p">.</span><span class="nf">set_completeness_percentage</span>
<span class="n">profile</span><span class="p">.</span><span class="nf">save!</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<h2>Be aware of race conditions</h2>
<p>When saving computed values in the database in your rails app, you must be aware that is possible to find unexpected errors in the result thanks to race conditions.</p>
<p>I have other article that can help you visualize how race conditions can make you save incorrect values even when your calculation is correct.</p>
<p>You can read it on: <a href="https://bhserna.com/saving-incorrect-computed-values-thanks-to-race-conditions.html">Saving incorrect computed values thanks to race conditions</a></p>
Tools to help you detect n+1 queries/tools-to-help-you-detect-n-1-queries.html2021-03-01T12:57:00Z2021-03-01T12:57:00ZBenito Serna<p>There are many tools that can help you detect n+1 queries in different ways.</p>
<p>This is a little reference of some of those tools:</p>
<ul>
<li><a href="#strict_loading">Strict loading</a></li>
<li><a href="#rack_mini_profiler">Rack mini profiler</a></li>
<li><a href="#bullet">Bullet</a></li>
<li><a href="#prosopite">Prosopite</a></li>
<li><a href="#n_plus_one_control">n plus one control</a></li>
</ul>
<p>You don’t need to use all of them, but is good to know that they exists and how they can help you.</p>
<p></p>
<h2 id="strict_loading">Rails strict_loading</h2>
<h3>How it can help you?</h3>
<p>You can add <code>#strict_loading!</code> to any record to prevent lazy loading of
associations. Strict loading will cascade down from the parent record to all the
associations to help you catch any places where you may want to use preload
instead of lazy loading.</p>
<p>On a record:</p>
<pre><code class="highlight ruby"><span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">first</span>
<span class="n">user</span><span class="p">.</span><span class="nf">strict_loading!</span>
<span class="n">user</span><span class="p">.</span><span class="nf">comments</span><span class="p">.</span><span class="nf">to_a</span>
<span class="o">=></span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">StrictLoadingViolationError</span>
</code></pre>
<p>On a relation:</p>
<pre><code class="highlight ruby"><span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">strict_loading</span><span class="p">.</span><span class="nf">first</span>
<span class="n">user</span><span class="p">.</span><span class="nf">comments</span><span class="p">.</span><span class="nf">to_a</span>
<span class="o">=></span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">StrictLoadingViolationError</span>
</code></pre>
<p>On an association definition:</p>
<pre><code class="highlight ruby"><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">has_many</span> <span class="p">:</span><span class="n">comments</span><span class="p">,</span> <span class="ss">strict_loading: </span><span class="kp">true</span>
<span class="k">end</span>
<span class="k">class</span> <span class="nc">Comment</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">belongs_to</span> <span class="p">:</span><span class="n">user</span>
<span class="k">end</span>
<span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">first</span>
<span class="n">user</span><span class="p">.</span><span class="nf">comments</span><span class="p">.</span><span class="nf">to_a</span>
<span class="o">=></span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">StrictLoadingViolationError</span>
</code></pre>
<p>Per model configuration:</p>
<pre><code class="highlight ruby"><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">strict_loading_by_default</span> <span class="o">=</span> <span class="kp">true</span>
<span class="n">has_many</span> <span class="p">:</span><span class="n">comments</span>
<span class="k">end</span>
<span class="k">class</span> <span class="nc">Comment</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">belongs_to</span> <span class="p">:</span><span class="n">user</span>
<span class="k">end</span>
<span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">first</span>
<span class="n">user</span><span class="p">.</span><span class="nf">comments</span><span class="p">.</span><span class="nf">to_a</span>
<span class="o">=></span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">StrictLoadingViolationError</span>
</code></pre>
<p>If you want to enable strict loading by default you can do it with:</p>
<pre><code class="highlight ruby"><span class="n">config</span><span class="p">.</span><span class="nf">active_record</span><span class="p">.</span><span class="nf">strict_loading_by_default</span> <span class="o">=</span> <span class="kp">true</span>
</code></pre>
<p>If you can change the action on strict loading violation from <code>:raise</code> (default) to <code>:log</code> you can do it with:</p>
<pre><code class="highlight ruby"><span class="n">config</span><span class="p">.</span><span class="nf">active_record</span><span class="p">.</span><span class="nf">action_on_strict_loading_violation</span> <span class="o">=</span> <span class="ss">:log</span>
</code></pre>
<h3>References</h3>
<ul>
<li><a href="https://api.rubyonrails.org/classes/ActiveRecord/Core.html#method-i-strict_loading-21">ActiveRecord::Core</a></li>
<li><a href="https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-strict_loading">ActiveRecord::QueryMethods</a></li>
<li><a href="https://github.com/rails/rails/pull/37400">Pull request: Add <code>strict_loading</code> mode to optionally prevent lazy loading</a></li>
</ul>
<h2 id="rack_mini_profiler">Rack mini profiler</h2>
<h3>How it can help you?</h3>
<p>It can help you with more than just detecting n+1 queries. It is a production and development profiler, it allows you to quickly isolate performance bottlenecks, both on the server and client.</p>
<p>It can help you with:</p>
<ul>
<li>Database profiling</li>
<li>Call-stack profiling</li>
<li>Memory profiling</li>
</ul>
<h3>How does the report looks like?</h3>
<p>It displays a speed badge for every html page that if you click it, it will show you a page with the profiling information of the current page.</p>
<p><img alt="miniprofiler" src="/2021-03-01-toolt-to-help-you-detect-n-1-queries/miniprofiler.png" /></p>
<p>And if you click on of the sql queries count, it will show you a list with all the queries…</p>
<p><img alt="miniprofiler" src="/2021-03-01-toolt-to-help-you-detect-n-1-queries/miniprofiler-queries.png" /></p>
<p>Although it will not tell you exactly that you have an n+1 quieries problem, it can help you a lot to visualize it.</p>
<h3>References</h3>
<ul>
<li><a href="https://github.com/MiniProfiler/rack-mini-profiler">github/MiniProfiler/rack-mini-profiler</a></li>
<li><a href="https://samsaffron.com/archive/2012/07/12/miniprofiler-ruby-edition">The announcement posts from 2012</a></li>
</ul>
<h2 id="bullet">Bullet</h2>
<h3>How can it help you?</h3>
<p>It will watch your queries while you develop your application and notify you when it detects one of this problems:</p>
<ul>
<li>n+1 queries</li>
<li>eager-loaded associations which are not used</li>
<li>unnecessary COUNT queries which could be avoided with a counter cache</li>
</ul>
<p>You can use it on the development and testing environments.</p>
<p>Sometimes Bullet may notify you of query problems you don’t care to fix, or which come from outside your code. You can whitelist these to ignore them.</p>
<h3>How does the error report looks like?</h3>
<p>For n+1 queries…</p>
<pre><code class="highlight plaintext">GET /posts
USE eager loading detected
Post => [:comments]
Add to your query: .includes([:comments])
Call stack
/Users/benitoserna/code/bullet-test/app/views/posts/index.html.erb:20:in `map'
/Users/benitoserna/code/bullet-test/app/views/posts/index.html.erb:20:in `block in _app_views_posts_index_html_erb__1178069968615334744_70147771830640'
/Users/benitoserna/code/bullet-test/app/views/posts/index.html.erb:16:in `_app_views_posts_index_html_erb__1178069968615334744_70147771830640'
</code></pre>
<p>For unused eager loading…</p>
<pre><code class="highlight plaintext">GET /posts
AVOID eager loading detected
Post => [:comments]
Remove from your query: .includes([:comments])
Call stack
</code></pre>
<h3>References</h3>
<ul>
<li><a href="https://github.com/flyerhzm/bullet">github/flyerhzm/bullet</a></li>
</ul>
<h2 id="prosopite">Prosopite</h2>
<h3>How can it help you?</h3>
<p>Prosopite is able to auto-detect Rails N+1 queries, but it also can help you
detect some cases where bullet will give you false positives or false
negatives.</p>
<p>Prosopite monitors all SQL queries using the Active Support instrumentation and
looks for a pattern which is present in all N+1 query cases: More than one
queries have the same call stack and the same query fingerprint.</p>
<p>You can use it on the development and testing environments.</p>
<p>Compared to bullet, Prosopite can auto-detect the following extra cases of N+1 queries:</p>
<ul>
<li>N+1 queries after record creations (usually in tests)</li>
</ul>
<pre><code class="highlight ruby"><span class="no">FactoryBot</span><span class="p">.</span><span class="nf">create_list</span><span class="p">(</span><span class="ss">:leg</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span>
<span class="no">Leg</span><span class="p">.</span><span class="nf">last</span><span class="p">(</span><span class="mi">10</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">l</span><span class="o">|</span>
<span class="n">l</span><span class="p">.</span><span class="nf">chair</span>
<span class="k">end</span>
</code></pre>
<ul>
<li>Not triggered by ActiveRecord associations</li>
</ul>
<pre><code class="highlight ruby"><span class="no">Leg</span><span class="p">.</span><span class="nf">last</span><span class="p">(</span><span class="mi">4</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">l</span><span class="o">|</span>
<span class="no">Chair</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">l</span><span class="p">.</span><span class="nf">chair_id</span><span class="p">)</span>
<span class="k">end</span>
</code></pre>
<ul>
<li>First/last/pluck of collection associations</li>
</ul>
<pre><code class="highlight ruby"><span class="no">Chair</span><span class="p">.</span><span class="nf">last</span><span class="p">(</span><span class="mi">20</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">c</span><span class="o">|</span>
<span class="n">c</span><span class="p">.</span><span class="nf">legs</span><span class="p">.</span><span class="nf">first</span>
<span class="n">c</span><span class="p">.</span><span class="nf">legs</span><span class="p">.</span><span class="nf">last</span>
<span class="n">c</span><span class="p">.</span><span class="nf">legs</span><span class="p">.</span><span class="nf">pluck</span><span class="p">(</span><span class="ss">:id</span><span class="p">)</span>
<span class="k">end</span>
</code></pre>
<ul>
<li>Changing the ActiveRecord class with #becomes</li>
</ul>
<pre><code class="highlight ruby"><span class="no">Chair</span><span class="p">.</span><span class="nf">last</span><span class="p">(</span><span class="mi">20</span><span class="p">).</span><span class="nf">map</span><span class="p">{</span> <span class="o">|</span><span class="n">c</span><span class="o">|</span> <span class="n">c</span><span class="p">.</span><span class="nf">becomes</span><span class="p">(</span><span class="no">ArmChair</span><span class="p">)</span> <span class="p">}.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">ac</span><span class="o">|</span>
<span class="n">ac</span><span class="p">.</span><span class="nf">legs</span><span class="p">.</span><span class="nf">map</span><span class="p">(</span><span class="o">&</span><span class="ss">:id</span><span class="p">)</span>
<span class="k">end</span>
</code></pre>
<ul>
<li>Mongoid models calling ActiveRecord</li>
</ul>
<pre><code class="highlight ruby"><span class="k">class</span> <span class="nc">Leg</span><span class="o">::</span><span class="no">Design</span>
<span class="kp">include</span> <span class="no">Mongoid</span><span class="o">::</span><span class="no">Document</span>
<span class="p">.</span><span class="nf">.</span><span class="p">.</span>
<span class="nf">field</span> <span class="ss">:cid</span><span class="p">,</span> <span class="ss">as: :chair_id</span><span class="p">,</span> <span class="ss">type: </span><span class="no">Integer</span>
<span class="p">.</span><span class="nf">.</span><span class="p">.</span>
<span class="nf">def</span> <span class="n">chair</span>
<span class="vi">@chair</span> <span class="o">||=</span> <span class="no">Chair</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">id: </span><span class="n">chair_id</span><span class="p">).</span><span class="nf">first!</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="no">Leg</span><span class="o">::</span><span class="no">Design</span><span class="p">.</span><span class="nf">last</span><span class="p">(</span><span class="mi">20</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">l</span><span class="o">|</span>
<span class="n">l</span><span class="p">.</span><span class="nf">chair</span>
<span class="k">end</span>
</code></pre>
<h3>How does the error report looks like?</h3>
<p>The report will show you the N+1 queries detected and the call stack.</p>
<pre><code class="highlight plaintext">N+1 queries detected:
SELECT `users`.* FROM `users` WHERE `users`.`id` = 20 LIMIT 1
SELECT `users`.* FROM `users` WHERE `users`.`id` = 21 LIMIT 1
SELECT `users`.* FROM `users` WHERE `users`.`id` = 22 LIMIT 1
SELECT `users`.* FROM `users` WHERE `users`.`id` = 23 LIMIT 1
SELECT `users`.* FROM `users` WHERE `users`.`id` = 24 LIMIT 1
Call stack:
app/controllers/thank_you_controller.rb:4:in `block in index'
app/controllers/thank_you_controller.rb:3:in `each'
app/controllers/thank_you_controller.rb:3:in `index':
app/controllers/application_controller.rb:8:in `block in <class:ApplicationController>'
</code></pre>
<h3>References</h3>
<ul>
<li><a href="https://github.com/charkost/prosopite">github/charkost/prosopite</a></li>
</ul>
<h2 id="n_plus_one_control">NPlusOneControl</h2>
<h3>How can it help you?</h3>
<p>It gives you rspec and minitest matchers to prevent the n+1 queries problem.</p>
<p>It evaluates the code under consideration several times with different scale
factors to make sure that the number of DB queries behaves as expected (i.e.
O(1) instead of O(N)).</p>
<p>So, it’s for performance testing and not feature testing.</p>
<h3>How does the error report looks like?</h3>
<p>In the default mode it can give you something like this.</p>
<pre><code class="highlight plaintext">Expected to make the same number of queries, but got:
10 for N=2
11 for N=3
Unmatched query numbers by tables:
resources (SELECT): 2 != 3
permissions (SELECT): 4 != 6
</code></pre>
<p>And in the “verbose” mode, it can give you something like this…</p>
<pre><code class="highlight plaintext">Expected to make the same number of queries, but got:
2 for N=2
3 for N=3
Unmatched query numbers by tables:
resources (SELECT): 2 != 3
Queries for N=2
SELECT "resources".* FROM "resources" WHERE "resources"."deleted_at" IS NULL
↳ app/controllers/resources_controller.rb:32:in `index'
...
Queries for N=3
...
</code></pre>
<h3>References</h3>
<ul>
<li><a href="https://github.com/palkan/n_plus_one_control">github/palkan/n plus one control</a></li>
<li><a href="https://evilmartians.com/chronicles/squash-n-plus-one-queries-early-with-n-plus-one-control-test-matchers-for-ruby-and-rails">Squash N+1 queries early with n plus one control test matchers for Ruby and Rails</a></li>
</ul>
Truncate in the middle with truncate rails helper/truncate-in-the-middle-with-truncate-rails-helper.html2023-06-19T23:08:00Z2023-06-19T23:08:00ZBenito Serna<p>Imagine that you want to truncate a filename, but you want to keep showing the extension of the file. Like “A big file name that…awesome.pdf”. How would you do it?</p>
<p></p>
<h2>The omission parameter</h2>
<p>With the <code>:omission</code> string the last characters will be replaced (defaults to “…”) for a total length not exceeding <code>length</code>:</p>
<pre><code class="highlight ruby"><span class="n">string</span> <span class="o">=</span> <span class="s2">"And they found that many people were sleeping better."</span>
<span class="n">string</span><span class="p">.</span><span class="nf">truncate</span><span class="p">(</span><span class="mi">25</span><span class="p">,</span> <span class="ss">omission: </span><span class="s1">'... (continued)'</span><span class="p">)</span>
<span class="c1"># => "And they f... (continued)"</span>
</code></pre>
<p>So, you can pass as the omssion string, the last characters of your string like this:</p>
<pre><code class="highlight ruby"><span class="n">filename</span> <span class="o">=</span> <span class="s2">"And they found that many people were sleeping better.pdf"</span>
<span class="n">filename</span><span class="p">.</span><span class="nf">truncate</span><span class="p">(</span><span class="mi">25</span><span class="p">,</span> <span class="ss">omission: </span><span class="s2">"... </span><span class="si">#{</span><span class="n">filename</span><span class="p">.</span><span class="nf">last</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="c1"># => "And they fo... better.pdf"</span>
</code></pre>
A form with two buttons with formation and formmethod/a-form-with-two-buttons-with-formation-and-formmethod.html2023-06-08T12:17:00Z2023-06-08T12:17:00ZBenito Serna<p>Imagine that you are building a custom CMS. Within the form to edit an <code>Article</code>, you need to have two buttons: a normal “Save” button and a new “Save and publish” button. And maybe, additionally, you will need a third button to delete the article.</p>
<p>To achieve this, you can use the <code>formaction</code> and <code>formmethod</code> attributes.</p>
<h3>Changing the action with <code>formaction</code></h3>
<p>To add the “Save and publish” button, use the <code>formaction</code> attribute to override the action in the form:</p>
<pre><code class="highlight erb"><span class="cp"><%=</span> <span class="n">form_with</span> <span class="ss">model: </span><span class="n">article</span> <span class="k">do</span> <span class="o">|</span><span class="n">form</span><span class="o">|</span> <span class="cp">%></span>
...
<span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">button</span> <span class="s2">"Save"</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">button</span> <span class="s2">"Save and publish"</span><span class="p">,</span> <span class="ss">formaction: </span><span class="n">save_and_publish_article_path</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
</code></pre>
<p>This sends the form, along with all the field information, to the path you defined in <code>formaction</code>. To process the information in a new controller action, define a route:</p>
<pre><code class="highlight ruby"><span class="n">resources</span> <span class="p">:</span><span class="n">articles</span> <span class="k">do</span>
<span class="n">patch</span> <span class="p">:</span><span class="n">save_and_publish</span><span class="p">,</span> <span class="ss">on: :member</span>
<span class="k">end</span>
</code></pre>
<p>Then, create the new controller action:</p>
<pre><code class="highlight ruby"><span class="k">def</span> <span class="nf">save_and_publish</span>
<span class="k">if</span> <span class="vi">@article</span><span class="p">.</span><span class="nf">update_and_publish</span><span class="p">(</span><span class="n">article_params</span><span class="p">)</span>
<span class="n">redirect_to</span> <span class="n">article</span>
<span class="k">else</span>
<span class="n">render</span> <span class="p">:</span><span class="n">edit</span><span class="p">,</span> <span class="ss">status: :unprocessable_entity</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<h3>Changing the method with <code>formmethod</code></h3>
<p>To add the “Delete” button, use the <code>formmethod</code> attribute to let HTML override the declared method attribute:</p>
<pre><code class="highlight erb"><span class="cp"><%=</span> <span class="n">form_with</span> <span class="ss">model: </span><span class="n">article</span> <span class="k">do</span> <span class="o">|</span><span class="n">form</span><span class="o">|</span> <span class="cp">%></span>
...
<span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">button</span> <span class="s2">"Save"</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">button</span> <span class="s2">"Delete"</span><span class="p">,</span> <span class="ss">formmethod: :delete</span><span class="p">,</span> <span class="ss">data: </span><span class="p">{</span> <span class="ss">confirm: </span><span class="s2">"Are you sure?"</span> <span class="p">}</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
</code></pre>
<p>Most browsers don’t support overriding form methods declared through <code>formmethod</code> other than “GET” and “POST”. Rails works around this issue by emulating other methods over POST through a combination of <code>formmethod</code>, <code>value</code>, and <code>name</code> attributes.</p>
<pre><code class="highlight html"><span class="nt"><form</span> <span class="na">accept-charset=</span><span class="s">"UTF-8"</span> <span class="na">action=</span><span class="s">"/articles/1"</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">></span>
<span class="nt"><input</span> <span class="na">name=</span><span class="s">"_method"</span> <span class="na">type=</span><span class="s">"hidden"</span> <span class="na">value=</span><span class="s">"patch"</span> <span class="nt">/></span>
<span class="nt"><input</span> <span class="na">name=</span><span class="s">"authenticity_token"</span> <span class="na">type=</span><span class="s">"hidden"</span> <span class="na">value=</span><span class="s">"..."</span> <span class="nt">/></span>
<span class="c"><!-- ... --></span>
<span class="nt"><button</span> <span class="na">type=</span><span class="s">"submit"</span> <span class="na">name=</span><span class="s">"button"</span><span class="nt">></span>Save<span class="nt"></button></span>
<span class="nt"><button</span> <span class="na">type=</span><span class="s">"submit"</span> <span class="na">formmethod=</span><span class="s">"post"</span> <span class="na">name=</span><span class="s">"_method"</span> <span class="na">value=</span><span class="s">"delete"</span> <span class="na">data-confirm=</span><span class="s">"Are you sure?"</span><span class="nt">></span>Delete<span class="nt"></button></span>
<span class="nt"></form></span>
</code></pre>
What to do when you need a button_to within a form in Rails/what-to-do-when-you-need-a-button_to-within-a-form-in-rails.html2023-06-05T12:12:00Z2023-06-05T12:12:00ZBenito Serna<p>Imagine that you have a form to update a record (let’s say a product record) and inside the form, you are showing a list of images, and each image needs a button to remove it. You tried to use button_to but it doesn’t work because in html you can have form within a form. What do you do?</p>
<h2>You can use the “form” attribute</h2>
<p>You can use a <code>button</code> tag and define its <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#form">form attribute</a>. The value should be the <code>id</code> of a form located anywhere in the document.</p>
<p>For example, in the next code, for each persited image, we call render a <code>button</code> with a form attribute <code>“delete_image_#{image.id}”</code>.</p>
<pre><code class="highlight erb"><span class="cp"><%=</span> <span class="n">form_with</span><span class="p">(</span><span class="ss">model: </span><span class="n">product</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">form</span><span class="o">|</span> <span class="cp">%></span>
...
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"product-images"</span><span class="nt">></span>
<span class="cp"><%</span> <span class="n">product</span><span class="p">.</span><span class="nf">persisted_images</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">image</span><span class="o">|</span> <span class="cp">%></span>
<span class="nt"><figure></span>
<span class="cp"><%=</span> <span class="n">image_tag</span> <span class="n">image</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">tag</span><span class="p">.</span><span class="nf">button</span><span class="p">(</span>
<span class="s2">"Remove"</span><span class="p">,</span>
<span class="ss">type: </span><span class="s2">"submit"</span><span class="p">,</span>
<span class="ss">form: </span><span class="s2">"delete_image_</span><span class="si">#{</span><span class="n">image</span><span class="p">.</span><span class="nf">id</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span> <span class="cp">%></span>
<span class="nt"></figure></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="nt"></div></span>
...
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
</code></pre>
<p>And later in the page, for each persisted image, we can render a hidden form with id “delete<em>image</em>#{image.id}” that will match the previous button.</p>
<pre><code class="highlight erb"><span class="cp"><%</span> <span class="n">product</span><span class="p">.</span><span class="nf">persisted_images</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">image</span><span class="o">|</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">form_with</span><span class="p">(</span>
<span class="ss">model: </span><span class="n">image</span><span class="p">,</span>
<span class="ss">url: </span><span class="n">product_image_path</span><span class="p">(</span><span class="n">product</span><span class="p">,</span> <span class="n">image</span><span class="p">),</span>
<span class="ss">method: :delete</span><span class="p">,</span>
<span class="ss">id: </span><span class="s2">"delete_image_</span><span class="si">#{</span><span class="n">image</span><span class="p">.</span><span class="nf">id</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span> <span class="k">do</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
</code></pre>
Simple image manager with active storage/simple-image-manager-with-active-storage.html2023-05-22T23:25:00Z2023-05-22T23:25:00ZBenito Serna<p>If you want to add images to a record but you don’t want to use a JavaScript plugin or write any custom JavaScript, you can use a regular file field, Active Storage, and vanilla Rails.</p>
<p></p>
<p>If you want to be able to:</p>
<ul>
<li>Attach many images on create</li>
<li>Keep adding many images on every update</li>
<li>Display the images in a gallery</li>
<li>Be able to remove the images one by one</li>
</ul>
<p>Here is a tutorial to help you accomplish that.</p>
<h2>Example: A product with many attached images</h2>
<p>To use as an example for the tutorial, we are going to have a <code>Product</code> record that has many attached images like this:</p>
<pre><code class="highlight ruby"><span class="k">class</span> <span class="nc">Product</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">has_many_attached</span> <span class="p">:</span><span class="n">images</span>
<span class="k">end</span>
</code></pre>
<h2>Add many images on create and every update</h2>
<p>If you want to add many attachments to a record using just a file field, but you don’t want to remove the previous images from the record on every update, you can use a virtual attribute “new_images”.</p>
<p>Instead of using the <code>:images</code> attribute, you can add a virtual <code>:new_images</code> attribute, using an <code>attr_reader</code> and a custom writer like this:</p>
<pre><code class="highlight ruby"><span class="k">class</span> <span class="nc">Product</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="kp">attr_reader</span> <span class="ss">:new_images</span>
<span class="n">has_many_attached</span> <span class="p">:</span><span class="n">images</span>
<span class="k">def</span> <span class="nf">new_images</span><span class="o">=</span><span class="p">(</span><span class="n">images</span><span class="p">)</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">images</span><span class="p">.</span><span class="nf">attach</span><span class="p">(</span><span class="n">images</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<p>And use that attribute in your <code>file_field</code>:</p>
<pre><code class="highlight erb"><span class="cp"><%=</span> <span class="n">form_with</span><span class="p">(</span><span class="ss">model: </span><span class="n">product</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">form</span><span class="o">|</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:new_images</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">file_field</span> <span class="ss">:new_images</span><span class="p">,</span> <span class="ss">multiple: </span><span class="kp">true</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">submit</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
</code></pre>
<p>Also, update your “params method” in the controller:</p>
<pre><code class="highlight ruby"><span class="k">def</span> <span class="nf">create</span>
<span class="vi">@product</span> <span class="o">=</span> <span class="vi">@site</span><span class="p">.</span><span class="nf">products</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">product_params</span><span class="p">)</span>
<span class="c1">#...</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">update</span>
<span class="vi">@product</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="n">product_params</span><span class="p">)</span>
<span class="c1"># ...</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">product_params</span>
<span class="n">params</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:product</span><span class="p">).</span><span class="nf">permit</span><span class="p">(</span><span class="ss">new_images: </span><span class="p">[])</span>
<span class="k">end</span>
</code></pre>
<p>This way, when you assign new images, your record will attach the received images instead of updating the images field with the new images.</p>
<h2>Display the images</h2>
<p>To display the images, you can put an HTML like this:</p>
<pre><code class="highlight erb"><span class="nt"><div</span> <span class="na">class=</span><span class="s">"product-images"</span><span class="nt">></span>
<span class="cp"><%</span> <span class="n">product</span><span class="p">.</span><span class="nf">persisted_images</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">image</span><span class="o">|</span> <span class="cp">%></span>
<span class="nt"><figure></span>
<span class="cp"><%=</span> <span class="n">image_tag</span> <span class="n">image</span> <span class="cp">%></span>
<span class="nt"></figure></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="nt"></div></span>
</code></pre>
<p>Where “persisted_images” is:</p>
<pre><code class="highlight ruby"><span class="k">def</span> <span class="nf">persisted_images</span>
<span class="n">images</span><span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="o">&</span><span class="ss">:persisted?</span><span class="p">)</span>
<span class="k">end</span>
</code></pre>
<p>Maybe you don’t need this, but I use this HTML inside the form, and I had some troubles when the record was not valid; it was showing images without content, this solved my problem.</p>
<h2>Remove the images one by one</h2>
<p>If you are not displaying the images inside the form, maybe you can put a <code>button_to</code> remove the image near the image.</p>
<pre><code class="highlight erb"><span class="nt"><div</span> <span class="na">class=</span><span class="s">"product-images"</span><span class="nt">></span>
<span class="cp"><%</span> <span class="n">product</span><span class="p">.</span><span class="nf">persisted_images</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">image</span><span class="o">|</span> <span class="cp">%></span>
<span class="nt"><figure></span>
<span class="cp"><%=</span> <span class="n">image_tag</span> <span class="n">image</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">button_to</span><span class="p">(</span>
<span class="s2">"Remove"</span><span class="p">,</span>
<span class="n">product_image_path</span><span class="p">(</span><span class="n">product</span><span class="p">,</span> <span class="n">image</span><span class="p">),</span>
<span class="ss">method: :delete</span><span class="p">)</span> <span class="cp">%></span>
<span class="nt"></figure></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="nt"></div></span>
</code></pre>
<p>But if you want to put the images inside your form, you can’t use a <code>button_to</code> because you can’t have a form inside a form.</p>
<p>So one thing you can do is to have a button that will trigger a form that is outside the main form, like this:</p>
<pre><code class="highlight erb"><span class="cp"><%=</span> <span class="n">form_with</span><span class="p">(</span><span class="ss">model: </span><span class="n">product</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">form</span><span class="o">|</span> <span class="cp">%></span>
<span class="nt"><div></span>
<span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:new_images</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">file_field</span> <span class="ss">:new_images</span><span class="p">,</span> <span class="ss">multiple: </span><span class="kp">true</span> <span class="cp">%></span>
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"product-images"</span><span class="nt">></span>
<span class="cp"><%</span> <span class="n">product</span><span class="p">.</span><span class="nf">persisted_images</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">image</span><span class="o">|</span> <span class="cp">%></span>
<span class="nt"><figure></span>
<span class="cp"><%=</span> <span class="n">image_tag</span> <span class="n">image</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">tag</span><span class="p">.</span><span class="nf">button</span><span class="p">(</span>
<span class="s2">"Remove"</span><span class="p">,</span>
<span class="ss">type: </span><span class="s2">"submit"</span><span class="p">,</span>
<span class="ss">form: </span><span class="s2">"delete_image_</span><span class="si">#{</span><span class="n">image</span><span class="p">.</span><span class="nf">id</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span> <span class="cp">%></span>
<span class="nt"></figure></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="nt"></div></span>
<span class="nt"><div></span>
<span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">submit</span> <span class="cp">%></span>
<span class="nt"></div></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="n">product</span><span class="p">.</span><span class="nf">persisted_images</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">image</span><span class="o">|</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">form_with</span><span class="p">(</span>
<span class="ss">model: </span><span class="n">image</span><span class="p">,</span>
<span class="ss">url: </span><span class="n">product_image_path</span><span class="p">(</span><span class="n">product</span><span class="p">,</span> <span class="n">image</span><span class="p">),</span>
<span class="ss">method: :delete</span><span class="p">,</span>
<span class="ss">id: </span><span class="s2">"delete_image_</span><span class="si">#{</span><span class="n">image</span><span class="p">.</span><span class="nf">id</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span> <span class="k">do</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
</code></pre>
<p>In the form, we are using a <code>button</code> tag of type <code>submit</code> that will trigger a form with id <code>"delete_image_#{image.id}"</code>.</p>
<pre><code class="highlight erb"><span class="cp"><%=</span> <span class="n">tag</span><span class="p">.</span><span class="nf">button</span><span class="p">(</span>
<span class="s2">"Remove"</span><span class="p">,</span>
<span class="ss">type: </span><span class="s2">"submit"</span><span class="p">,</span>
<span class="ss">form: </span><span class="s2">"delete_image_</span><span class="si">#{</span><span class="n">image</span><span class="p">.</span><span class="nf">id</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span> <span class="cp">%></span>
</code></pre>
<p>And we are creating a form for each image specifying that id:</p>
<pre><code class="highlight erb"><span class="cp"><%</span> <span class="n">product</span><span class="p">.</span><span class="nf">persisted_images</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">image</span><span class="o">|</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">form_with</span><span class="p">(</span>
<span class="ss">model: </span><span class="n">image</span><span class="p">,</span>
<span class="ss">url: </span><span class="n">product_image_path</span><span class="p">(</span><span class="n">product</span><span class="p">,</span> <span class="n">image</span><span class="p">),</span>
<span class="ss">method: :delete</span><span class="p">,</span>
<span class="ss">id: </span><span class="s2">"delete_image_</span><span class="si">#{</span><span class="n">image</span><span class="p">.</span><span class="nf">id</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span> <span class="k">do</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
</code></pre>
<p>Then on the controller that handles the <code>product_image_path</code>, you can remove the image with <code>purge</code>.</p>
<pre><code class="highlight ruby"><span class="k">class</span> <span class="nc">ImagesController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="n">before_action</span> <span class="p">:</span><span class="n">set_product</span>
<span class="k">def</span> <span class="nf">destroy</span>
<span class="vi">@product</span><span class="p">.</span><span class="nf">images</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">]).</span><span class="nf">purge</span>
<span class="c1">#...</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">set_product</span>
<span class="vi">@product</span> <span class="o">=</span> <span class="no">Product</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:product_id</span><span class="p">])</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<h2>Sample Code</h2>
<p>I have a sample application with code available at <a href="https://github.com/bhserna/simple_image_managment">https://github.com/bhserna/simple<em>image</em>managment</a>.</p>
Add many attachments without deleting previous ones using ActiveStorage/add-many-attachments-without-deleting-previous-ones-using-activestorage.html2023-05-16T12:09:00Z2023-05-16T12:09:00ZBenito Serna<p>If you want to add many attachments to a record using just a file field, but you don’t want to remove the previous images from the record on every update, like in the following code:</p>
<p></p>
<pre><code class="highlight erb"><span class="cp"><%=</span> <span class="n">form_with</span><span class="p">(</span><span class="ss">model: </span><span class="n">product</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">form</span><span class="o">|</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:images</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">file_field</span> <span class="ss">:images</span><span class="p">,</span> <span class="ss">multiple: </span><span class="kp">true</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">submit</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
</code></pre>
<p>And instead, on every update you want to add the new images to the record, here is a simple workaround…</p>
<h2>A virtual new_images attribute</h2>
<p>Instead of using the <code>:images</code> attribute, you can add a virtual <code>:new_images</code> attribute, using an <code>attr_reader</code> and a custom writer like this:</p>
<pre><code class="highlight ruby"><span class="k">class</span> <span class="nc">Product</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="kp">attr_reader</span> <span class="ss">:new_images</span>
<span class="n">has_many_attached</span> <span class="p">:</span><span class="n">images</span>
<span class="k">def</span> <span class="nf">new_images</span><span class="o">=</span><span class="p">(</span><span class="n">images</span><span class="p">)</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">images</span><span class="p">.</span><span class="nf">attach</span><span class="p">(</span><span class="n">images</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<p>And use that attribute in your <code>file_field</code>:</p>
<pre><code class="highlight erb"><span class="cp"><%=</span> <span class="n">form_with</span><span class="p">(</span><span class="ss">model: </span><span class="n">product</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">form</span><span class="o">|</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:new_images</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">file_field</span> <span class="ss">:new_images</span><span class="p">,</span> <span class="ss">multiple: </span><span class="kp">true</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">submit</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
</code></pre>
<p>Also, update your “params method” in the controller:</p>
<pre><code class="highlight ruby"><span class="k">def</span> <span class="nf">create</span>
<span class="vi">@product</span> <span class="o">=</span> <span class="vi">@site</span><span class="p">.</span><span class="nf">products</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">product_params</span><span class="p">)</span>
<span class="c1">#...</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">update</span>
<span class="vi">@product</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="n">product_params</span><span class="p">)</span>
<span class="c1"># ...</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">product_params</span>
<span class="n">params</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:product</span><span class="p">).</span><span class="nf">permit</span><span class="p">(</span><span class="ss">new_images: </span><span class="p">[])</span>
<span class="k">end</span>
</code></pre>
<p>This way, when you assign new images, your record will attach the received images instead of updating the images field with the new images.</p>
Use after_touch to avoid to handle race conditions saving computed values/use-after_touch-to-avoid-to-handle-race-conditions-saving-computed-values.html2023-03-02T23:16:00Z2023-03-02T23:16:00ZBenito Serna<p>When saving computed values in the database in your rails app, you <a href="https://bhserna.com/saving-incorrect-computed-values-thanks-to-race-conditions.html">must be aware that is possible to find unexpected errors in the result thanks to race conditions.</a></p>
<p>I have already <a href="https://bhserna.com/will-it-save-the-wrong-value.html">shared an exercise</a> to help you get more sensitivity about when an implementation can save a wrong value thanks to race conditions.</p>
<p>Here I want to share one tip to help you avoid save the wrong value due race conditions while trying to save a computed value.</p>
<h2>Using the account balance as an example</h2>
<p>To talk about something concrete I will use the “account balance” as an example, but you can use this approach for different types of calculations.</p>
<h2>The account balance example</h2>
<p>Imagine that you have an <code>Account</code> record that has many <code>entries</code>, and you want to update the <code>balance</code> each time an <code>Entry</code> is created. The <code>balance</code> is the sum of the <code>amount</code> of each <code>entry</code>.</p>
<p>Imagine each <strong>account will need to create many entries concurrently,</strong> maybe on different background jobs or different requests. So if you want to calculate the balance and save it just after an entry is created, you could have problems with race conditions.</p>
<h2>Tip: Use after_touch to trigger the operation</h2>
<p>As far as I understand, if you are using Postgres with the default isolation level <a href="https://www.postgresql.org/docs/current/transaction-iso.html#XACT-READ-COMMITTED">Read Committed</a>, you can compute and save the value on an <code>after_touch</code> like this:</p>
<pre><code class="highlight ruby"><span class="k">class</span> <span class="nc">Account</span> <span class="o"><</span> <span class="no">ExampleRecord</span>
<span class="n">has_many</span> <span class="p">:</span><span class="n">entries</span>
<span class="n">after_touch</span> <span class="p">:</span><span class="n">update_balance</span>
<span class="k">def</span> <span class="nf">create_entry</span><span class="p">(</span><span class="n">amount</span><span class="p">:)</span>
<span class="n">entries</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">amount: </span><span class="n">amount</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">update_balance</span>
<span class="n">balance</span> <span class="o">=</span> <span class="n">entries</span><span class="p">.</span><span class="nf">balance</span>
<span class="n">update!</span><span class="p">(</span><span class="ss">balance: </span><span class="n">balance</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">class</span> <span class="nc">Entry</span> <span class="o"><</span> <span class="no">ExampleRecord</span>
<span class="n">belongs_to</span> <span class="p">:</span><span class="n">account</span><span class="p">,</span> <span class="ss">touch: </span><span class="kp">true</span>
<span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">balance</span>
<span class="n">sum</span><span class="p">(</span><span class="ss">:amount</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<h2>Why does it work?</h2>
<p>Or at least, why do I think that it works…</p>
<p>Apparently <code>after_touch</code> works something like this:</p>
<ul>
<li>There is a change in the association.</li>
<li>Within the same transaction, rails updates the associated record (<code>account</code>).</li>
<li>Then it performs the other operation that you have configured (<code>update_balance</code>).</li>
</ul>
<p>PostgreSQL uses by default the <a href="https://www.postgresql.org/docs/current/transaction-iso.html#XACT-READ-COMMITTED">Read Committed</a> isolation level.</p>
<p>In this mode (as far as I understand), if within a transaction there is an <code>UPDATE</code> of a row and in another concurrent transaction an <code>UPDATE</code> of the same row is attempted, PostgreSQL will wait for the first transaction to be committed before continuing with the second one.</p>
<p>This makes the operations we perform on <code>after_touch</code> to be isolated and avoids the problem of saving a wrong error due to another transaction registering another entry for the same account concurrently.</p>
<h2>Do you want to test it?</h2>
<p>You can check the <a href="https://bhserna.com/custom-computed-values-and-race-conditions-examples-rails.html">examples to explore possible race conditions when caching custom computed values in Rails</a>.</p>
<p>And more specifically you can test this <a href="https://github.com/bhserna/custom_computed_values_and_race_conditions/blob/main/examples/03_after_touch.rb">example</a> that uses <code>after_touch</code>.</p>
<h2>Are we avoiding race conditions?</h2>
<p>We are not avoiding race conditions, other problems can happen, but we are at least avoiding saving the wrong value due to race conditions.</p>
<h2>Is this a perfect fix?</h2>
<p>No, if the calculation of the value that you are trying to save takes a lot of time and your application is really concurrent, it can queue up many transactions and cause other problems.</p>
<p>Also the reason why it works, can be a little obscure.</p>
<h2>Do you know other problems with this solution?</h2>
<p>If you have experience with other problems with this solution, please leave a comment =)</p>