Take your Rails development to the next levelJekyll2024-03-14T12:54:09-04:00http://blog.widefix.com/Andrei Kaleshkahttp://blog.widefix.com/ka8725@gmail.comhttp://blog.widefix.com/prevent-account-sharing-with-mfa2024-03-14 15:57:54 +0100T00:00:00-00:002024-03-14T00:00:00-04:00Andrei Kaleshkahttp://blog.widefix.comka8725@gmail.com
<p>TL;DR: MFA increased our app new signups by 30% daily. It can help you but measure the impact of the changes.</p>
<p>Preventing account sharing in your app is vital. Different methods tackle this. Recently, we’ve been focusing on the issue. We discovered a simple, low-risk fix. It protects revenue and boosts security. Also, it aids in generating dynamic reports. This solution allows us to track data. It helps manage violation cases. Check out how we achieved this.</p>
<h2 id="why-to-prevent-sharing-account">Why to prevent sharing account</h2>
<p>Sharing an account is one of the backdoors that leads to financial losses. Instead of having paid two users who are friends in your app, you have only one. They buy one account and share credentials, resulting in underpaying the business.</p>
<p>Another aspect of preventing sharing accounts is security. Someone might steal a user’s credentials. The thief will use the account, and nobody will know about it.</p>
<p><img src="/images/security.jpg" alt="MFA is for security" /></p>
<p>All of that is very important indeed. However, our primary goal was to boost app revenue.</p>
<h2 id="prevent-account-sharing-with-a-third-party-service">Prevent account sharing with a third-party service</h2>
<p>Some services promise to solve the issue with account sharing in your product. That option might sound like a good choice. Relying on their implementation, you sign up. Then, you configure your app by following their instructions and enjoying it. Sounds easy and fantastic. Yet, some disadvantages can prevent you from following this path.</p>
<p>To start, consider how these services tackle the problem. They track user actions. These include clicks, mouse movements, IP addresses, device types, and locations. If the user’s behavior deviates from the norm, the service flags it as suspicious. While effective, there are limitations to this approach. It hinges on a vast amount of user data. The more data and users they have, the more accurate the service becomes. The law of large numbers must function. But you never know if they have enough data. They also need to know your app data and users. Hence, the conclusions about sharing an account might be wrong. In the end, remember that there will always be false positives. It’s essential to have a plan to address them.</p>
<p>This solution takes time to set up and integrate. Dealing with false positives is challenging. You need custom logic to link this service to your app. You lose control over valuable data to improve your product. Also, there’s a fee that may not suit long-term needs. Comparing it to your solution is necessary.</p>
<p>We aimed for user session control, limiting per-user sessions. Account sharing was still possible. The system permitted a fixed number of simultaneous sessions. A user accessing the app on various devices led us here. Working with a third-party service brought obstacles here. Thus, we chose to drop this method.</p>
<h2 id="our-path-to-a-solution-for-preventing-account-sharing">Our path to a solution for preventing account sharing</h2>
<p>As the first step, many recommend using multi-factor authentication (MFA) and stopping there. Indeed, it’s an excellent option to consider. We definitely should implement it. Yet, how can we be sure it helps? How can we prevent user loss after enabling MFA? Each app community is different. Their expectations may vary. Enabling MFA for all for no reason could alienate many users. They may leave the app, hurting the business.</p>
<p>Hence, we need indicators to track the impact of our changes on user behavior. Yet, we must decide who gets MFA enabled and assess whether account-sharing prevention works. Thus, an imaginary “violations per user” is a crucial indicator to track. Ultimately, it aligns with login sessions per user—the fewer active login sessions a user has, the fewer violations.
If all goes well, the “violations per user” indicator should drop after enabling MFA. Additionally, we expect more users to sign up. Some users using the shared account should eventually sign up for the app. Moreover, the number of active users should not drop. If all that happens, that would mean the feature works well and improves things rather than hurting the business.</p>
<p>We can easily track the number of new signups and the number of active users without changing the app. Having direct access to the database, we can do that with relatively easy SQL queries.</p>
<p>However, we do not have data for the “violations per user” indicator. Thus, we should implement a solution for that.</p>
<p>Thinking this way, we come up with the following initial ideas:</p>
<ul>
<li>Analyze the user agent’s IP and device type in the web server logs. Create a report of suspicious user actions such as often IP change or device type.</li>
<li>Then, if every subsequent request differs from what’s inside the cookies, it’s a violation.</li>
<li>After analyzing these reports, come up with an idea of who should have MFA enabled. Eventually, implement MFA for them.</li>
</ul>
<p><img src="/images/idea-bulb.jpg" alt="Bright idea" /></p>
<h2 id="solution-1-logs-analysis">Solution #1: logs analysis</h2>
<p>Looking into logs and checking how many unique IPs and devices one user uses is a natural solution that comes to mind. We have implemented it. However, this solution did not derive a “sharing account violations” indicator. The story behind this deserves a separate post. In short, it was a challenging task. Based on this data, we’ve had many false positive reports.
This is a challenge for us and giants like Google, LastPass, and even your local banks. For that reason, you get a lot of irritating emails every time you log in to these services. I do, and I have it. Our users could hate it as well. A more straightforward solution should correctly identify the fact of sharing accounts.</p>
<h2 id="solution-2-unique-token-per-device-written-in-cookies">Solution #2: unique token per device written in cookies</h2>
<p>If we have to limit the users’ login sessions, we could restrict them by device type. We can issue a unique token per user device and store it in the DB along with other important information such as IP and the user agent (device info). The idea was to allow one user to use only one device of a specific type at a time. For example, one user can use one web login on a desktop and one on a mobile app. If someone else uses the same device type at the time, it’s a violation. We successfully implemented this solution, and in fact, it worked well. There were some false positives. But fewer than in the previous solution. We could even stick with this solution. However, the business changed its mind and allowed a limited number of login sessions, independent of the device type.</p>
<p>Nevertheless, even though it’s been rejected, this solution can still be used to implement the “sharing account violations” indicator. We use it to collect and analyze reports.</p>
<h2 id="solution-3-login-session-concept">Solution #3: login session concept</h2>
<p>Using the experience of the previous solution, we implemented a modernized version that elaborated into a login session concept. The idea is the following. One user can have many login sessions. A login session is an entity with a unique token. It’s created right after the user’s login and deactivated after the user logs out. This unique token is injected into cookies. We search for an active login session in every subsequent request and create if it still needs to be added. A user with many such login sessions shares an account. And for them, we should enable MFA.</p>
<h2 id="solution-4-device-limit-limit-of-login-sessions">Solution #4: device limit, limit of login sessions</h2>
<p>Having the previous solution ready, we can limit the login sessions in the following way: Every time a user logs in, check if the number of login sessions is too big, say 4. If that’s the case, prevent the user from going further and ask them to log in from some of the current login sessions. This is what we are implementing next.</p>
<p><img src="/images/active-sessions.png" alt="Active login sessions" /></p>
<h2 id="our-results-analysis-and-reports">Our results, analysis, and reports</h2>
<p>Now, we have the indicator that shows the dynamic of account sharing violations. We gradually enable MFA for those users with the most violations and check the following dynamics.</p>
<p>New signups and active users on a paid plan. Note that it includes users on a trial that lasts for 15 days. That’s why there is this peak on the right side:</p>
<p><img src="/images/prevent-account-sharing/active-users-report.png" alt="Active login sessions" /></p>
<p>Sharing token reports dynamics:</p>
<p><img src="/images/prevent-account-sharing/token-violations-report.png" alt="Token violations report" /></p>
<p>The report dynamics are based on log analysis. It turned out to be useless, and for that reason, we will drop it:</p>
<p><img src="/images/prevent-account-sharing/log-analyzed-reports.png" alt="Log analyzed reports" /></p>
<p>Active login sessions per user with MFA enabled:</p>
<p><img src="/images/prevent-account-sharing/otp-enabled-reports.png" alt="MFA enabled reports" /></p>
<p>Active login sessions per user with MFA disabled:</p>
<p><img src="/images/prevent-account-sharing/otp-disabled-reports.png" alt="MFA disabled reports" /></p>
<p>Active login sessions for all users regardless of the MFA toggle. This one has the data combined with the 2 previous charts:</p>
<p><img src="/images/prevent-account-sharing/average-per-user-reports.png" alt="Regardless MFA toggle reports" /></p>
<p>Looking into these charts, we conclude that:</p>
<ul>
<li>The number of new paying signups increases. Roughly 30% more new signups daily.</li>
<li>The number of sharing account reports based on the token solution decreases. The regression line predicts a drop from 170 points to 120, almost a 30% drop when IP and device differ.</li>
<li>The number of login sessions per user decreases for users with MFA enabled. This improvement is significant compared to users with MFA disabled.</li>
</ul>
<h2 id="summary">Summary</h2>
<p>In this post, you learned how the positive impact of MFA enabling in your project can be measured. Now, you know which metrics inside your app you should watch for to understand if your solution works and helps the business survive.</p>
<p>Finally, you got a beneficial insight — MFA positively impacts user behavior. It indeed prevents users from sharing accounts. The improvement is significant — roughly 30% more new signups. Additionally, it’s a security improvement for your app. It is worth considering for your project. But keep in mind that your auditory can be different, so measure the impact.</p>
<p>In the next step, we will implement the login session limits. We expect an even more positive impact and will measure the results. Stay tuned!</p>
<p>Follow me on social networks not to miss the next post:</p>
<ul>
<li><a href="https://www.linkedin.com/in/ka8725">LinkedIn</a></li>
<li><a href="https://twitter.com/ka8725">Twitter</a></li>
<li><a href="https://github.com/ka8725">GitHub</a></li>
</ul>
<p><a href="http://blog.widefix.com/prevent-account-sharing-with-mfa/">Prevent account sharing with MFA</a> was originally published by Andrei Kaleshka at <a href="http://blog.widefix.com">Take your Rails development to the next level</a> on March 14, 2024.</p>http://blog.widefix.com/reconcile-app-users-against-stripe-and-prevent-financial-losses2023-01-30 18:23:20 +0100T00:00:00-00:002024-01-30T00:00:00-05:00Andrei Kaleshkahttp://blog.widefix.comka8725@gmail.com
<h2 id="possible-discrepancies-in-stripe-integrations">Possible discrepancies in Stripe integrations</h2>
<p>Stripe integrations often experience data consistency issues, such as users being on different subscription plans in the app compared to Stripe. This misalignment can lead to lost revenue or jeopardize customer success stories.</p>
<p>For example, a customer may have a premium status within the app, while Stripe shows no active subscription. This type of discrepancy can lead to lost revenue for the business. Another potential issue occurs when a customer overpays. In this case, the customer is on the free plan within the app but has an active subscription to the premium plan in Stripe, jeopardizing the customer success story.</p>
<h2 id="stripe-integration-state-of-our-application">Stripe integration state of our application</h2>
<p>This blog post outlines the approach we took to resolve such issues in our app, which had around 80k users, with 10% on the premium plan. We identified instances of both overpayment and underpayment, which could be attributed to manual data manipulation in Stripe, missing webhooks, or bugs in our system.</p>
<section style="background-color: rgba(240,67,56,.08); padding: 10px; padding-bottom: 20px; border-radius: 10px; margin-top: 20px; text-align: center; font-family: Arial, Helvetica, sans-serif;">
<strong class="seeking-assistance-magnete">Are you seeking assistance with Ruby on Rails development?</strong>
<div style="display: flex;align-items:center;justify-content: center;margin-top: 20px;">
<a class="schedule-button" style="background-color: #266cb4;" target="_blank" rel="nofollow" href="https://calendly.com/andrei-kaleshka/30min">Schedule a call</a>
</div>
</section>
<h2 id="the-plan-to-reconcile-data-with-stripe">The plan to reconcile data with Stripe</h2>
<p>While we used Ruby for scripting, this solution is universal and can be applied to applications written in other languages like Python, Node.js, Java, Rust, and Go.</p>
<p>Our plan is the following:</p>
<ol>
<li>Fetch all relevant data from Stripe via Stripe REST API. In our case that included all customer entities and their subscriptions.</li>
<li>Put the fetched data into our database. To avoid polluting the main database schema, use a separate namespace for these tables. In fact, PostgreSQL refers to this namespace concept as <code class="highlighter-rouge">schema</code>. The default schema is <code class="highlighter-rouge">public</code>. We create the <code class="highlighter-rouge">stripe</code> schema and necessary tables inside it.</li>
<li>Analyze the discrepancies using SQL queries. SQL is not only faster and less prone to bugs for this kind of task in terms of data processing but also in development. This means we will obtain results faster with fewer expenses for the business.</li>
<li>Configure Metabase for these reports. Metabase is the system we use to write SQL and build reports. Set up notifications for data discrepancies so that we receive alerts about new cases and can react to them as soon as possible. Luckily, it was already set up for this project. The good news is that its setup takes very little time.</li>
<li>Schedule the data scraper from Step 1 to run automatically every week.</li>
</ol>
<p><img src="/images/shining-plan.jpg" alt="Shining plan" /></p>
<h2 id="stripe-data-store-preparation">Stripe data store preparation</h2>
<p>First, we need to create the DB schema along with the Stripe tables. This is the SQL script we used for that:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">create</span> <span class="k">schema</span> <span class="n">stripe</span><span class="p">;</span>
<span class="k">create</span> <span class="k">table</span> <span class="n">stripe</span><span class="p">.</span><span class="n">customers</span> <span class="p">(</span>
<span class="n">id</span> <span class="nb">serial</span> <span class="k">not</span> <span class="k">null</span><span class="p">,</span>
<span class="n">stripe_id</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">250</span><span class="p">),</span>
<span class="n">email</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">250</span><span class="p">)</span> <span class="k">not</span> <span class="k">null</span><span class="p">,</span>
<span class="n">created_in_stripe</span> <span class="nb">timestamp</span><span class="p">,</span>
<span class="n">description</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">250</span><span class="p">),</span>
<span class="n">metadata</span> <span class="n">jsonb</span><span class="p">,</span>
<span class="n">deleted</span> <span class="nb">boolean</span><span class="p">,</span>
<span class="n">created_at</span> <span class="nb">timestamp</span> <span class="k">default</span> <span class="k">current_timestamp</span><span class="p">,</span>
<span class="n">updated_at</span> <span class="nb">timestamp</span> <span class="k">default</span> <span class="k">current_timestamp</span>
<span class="p">);</span>
<span class="k">create</span> <span class="k">table</span> <span class="n">stripe</span><span class="p">.</span><span class="n">subscriptions</span> <span class="p">(</span>
<span class="n">id</span> <span class="nb">serial</span> <span class="k">not</span> <span class="k">null</span><span class="p">,</span>
<span class="n">stripe_id</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">250</span><span class="p">),</span>
<span class="n">stripe_customer_id</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">250</span><span class="p">),</span>
<span class="n">plan_id</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">250</span><span class="p">),</span>
<span class="n">status</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">250</span><span class="p">),</span>
<span class="n">created_at</span> <span class="nb">timestamp</span> <span class="k">default</span> <span class="k">current_timestamp</span><span class="p">,</span>
<span class="n">updated_at</span> <span class="nb">timestamp</span> <span class="k">default</span> <span class="k">current_timestamp</span>
<span class="p">);</span>
</code></pre></div></div>
<blockquote>
<p>Inside the Rails app, data migrations could be created for this purpose. However, as we are unsure whether these tables are permanent at the moment, we use SQL and execute it manually on the server.</p>
</blockquote>
<p>Our app uses Rails, so it has <code class="highlighter-rouge">ActiveRecord</code>, and we can point our models to these tables. These are the <code class="highlighter-rouge">ActiveRecord</code> models we created for our convenience while working with these tables in the Ruby script:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">StripeSchema</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">abstract_class</span> <span class="o">=</span> <span class="kp">true</span>
<span class="n">establish_connection</span> <span class="ss">:stripe</span>
<span class="k">end</span>
<span class="k">class</span> <span class="nc">StripeCustomer</span> <span class="o"><</span> <span class="no">StripeSchema</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">table_name</span> <span class="o">=</span> <span class="ss">:customers</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">primary_key</span> <span class="o">=</span> <span class="ss">:id</span>
<span class="k">end</span>
<span class="k">class</span> <span class="nc">StripeSubscription</span> <span class="o"><</span> <span class="no">StripeSchema</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">table_name</span> <span class="o">=</span> <span class="ss">:subscriptions</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">primary_key</span> <span class="o">=</span> <span class="ss">:id</span>
<span class="n">belongs_to</span> <span class="ss">:stripe_customer</span><span class="p">,</span> <span class="ss">foreign_key: :stripe_id</span>
<span class="k">end</span>
</code></pre></div></div>
<p>We have a special folder in the app, <code class="highlighter-rouge">app/scripts</code>, where we put this kind of ad-hoc code. Later, we use Rails runner to execute them on the production server against the real data. They can also be run locally for testing purposes during script development. This script code was put into <code class="highlighter-rouge">app/scripts/stripe_classes.rb</code> file.</p>
<section style="background-color: rgba(240,67,56,.08); padding: 10px; padding-bottom: 20px; border-radius: 10px; margin-top: 20px; text-align: center; font-family: Arial, Helvetica, sans-serif;">
<strong class="seeking-assistance-magnete">Are you seeking assistance with Ruby on Rails development?</strong>
<div style="display: flex;align-items:center;justify-content: center;margin-top: 20px;">
<a class="schedule-button" style="background-color: #266cb4;" target="_blank" rel="nofollow" href="https://calendly.com/andrei-kaleshka/30min">Schedule a call</a>
</div>
</section>
<h2 id="the-stripe-data-scrapper">The Stripe data scrapper</h2>
<p>And this is the script we’ve come up with:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">starting_after</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="kp">loop</span> <span class="k">do</span>
<span class="n">params</span> <span class="o">=</span> <span class="p">{</span><span class="ss">expand: </span><span class="p">[</span><span class="s1">'data.subscriptions'</span><span class="p">],</span> <span class="ss">limit: </span><span class="mi">100</span><span class="p">}</span>
<span class="n">params</span><span class="p">[</span><span class="ss">:starting_after</span><span class="p">]</span> <span class="o">=</span> <span class="n">starting_after</span> <span class="k">if</span> <span class="n">starting_after</span>
<span class="n">customers</span> <span class="o">=</span> <span class="no">Stripe</span><span class="o">::</span><span class="no">Customer</span><span class="p">.</span><span class="nf">list</span><span class="p">(</span><span class="n">params</span><span class="p">)</span>
<span class="n">customers</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">cus</span><span class="o">|</span>
<span class="no">ApplicationRecord</span><span class="p">.</span><span class="nf">transaction</span> <span class="k">do</span>
<span class="no">StripeCustomer</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span>
<span class="ss">email: </span><span class="n">cus</span><span class="p">[</span><span class="s2">"email"</span><span class="p">],</span>
<span class="ss">stripe_id: </span><span class="n">cus</span><span class="p">[</span><span class="s2">"id"</span><span class="p">],</span>
<span class="ss">description: </span><span class="n">cus</span><span class="p">[</span><span class="s2">"description"</span><span class="p">],</span>
<span class="ss">created_in_stripe: </span><span class="n">cus</span><span class="p">[</span><span class="s2">"created"</span><span class="p">]</span> <span class="p">?</span> <span class="no">Time</span><span class="p">.</span><span class="nf">zone</span><span class="p">.</span><span class="nf">at</span><span class="p">(</span><span class="n">cus</span><span class="p">[</span><span class="s2">"created"</span><span class="p">])</span> <span class="p">:</span> <span class="kp">nil</span><span class="p">,</span>
<span class="ss">metadata: </span><span class="n">cus</span><span class="p">[</span><span class="s2">"metadata"</span><span class="p">],</span>
<span class="ss">subscriptions: </span><span class="n">customer_subscriptions</span><span class="p">,</span>
<span class="ss">deleted: </span><span class="n">cus</span><span class="p">[</span><span class="s2">"deleted"</span><span class="p">]</span>
<span class="p">)</span>
<span class="n">cus</span><span class="p">.</span><span class="nf">subscriptions</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="nb">sub</span><span class="o">|</span>
<span class="no">StripeSubscription</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span>
<span class="ss">stripe_customer_id: </span><span class="n">cus</span><span class="p">[</span><span class="s2">"id"</span><span class="p">],</span>
<span class="ss">stripe_id: </span><span class="nb">sub</span><span class="p">[</span><span class="s2">"id"</span><span class="p">],</span>
<span class="ss">plan_id: </span><span class="nb">sub</span><span class="p">[</span><span class="s2">"plan"</span><span class="p">][</span><span class="s2">"id"</span><span class="p">],</span>
<span class="ss">status: </span><span class="nb">sub</span><span class="p">[</span><span class="s2">"status"</span><span class="p">]</span>
<span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">starting_after</span> <span class="o">=</span> <span class="n">cus</span><span class="p">[</span><span class="s2">"id"</span><span class="p">]</span>
<span class="k">end</span>
<span class="k">break</span> <span class="k">if</span> <span class="n">customers</span><span class="p">.</span><span class="nf">count</span> <span class="o"><</span> <span class="mi">100</span>
<span class="k">end</span>
</code></pre></div></div>
<p>This script fetches all Stripe customers along with their subscriptions. Later, using this data, we can compare it against our app’s database and identify any deviations.</p>
<p>We run this script on the server with the <a href="https://guides.rubyonrails.org/command_line.html#bin-rails-runner">rails runner</a>. Its run can take a while. Not to get the process killed with a closed SSH connection to the server, we use <a href="https://www.gnu.org/software/screen/">screen</a> utility. At least for the first time, we have to run it manually and monitor for any failures. If there are any issues, we fix them. This way, we verify that the script is valid and reliable. Later, we can automate it to run on a schedule. We’ve collected data on 85k customers and 8.5k subscriptions, and it took around 1 hour. Not bad for this amount of data.</p>
<p><img src="/images/data-stripe.jpg" alt="Collected data from Stripe" /></p>
<h2 id="run-the-script-automatically">Run the script automatically</h2>
<p>After some preliminary data analysis and verification of its correctness, we can set up this script to run automatically on the server. We use sidekiq and its extension sidekiq-scheduler for this, as it’s already in place. The app also has Rollbar configured to monitor exceptions and errors, so if something goes wrong, we can be notified.</p>
<p>This is the job:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">UpdateStripeData</span>
<span class="kp">include</span> <span class="no">Sidekiq</span><span class="o">::</span><span class="no">Worker</span>
<span class="nb">require_relative</span> <span class="s1">'../../scripts/stripe_classes'</span>
<span class="k">def</span> <span class="nf">perform</span>
<span class="n">drop_old_data</span>
<span class="n">record_stripe_data</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">record_stripe_data</span>
<span class="n">starting_after</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="kp">loop</span> <span class="k">do</span>
<span class="n">params</span> <span class="o">=</span> <span class="p">{</span><span class="ss">expand: </span><span class="p">[</span><span class="s1">'data.subscriptions'</span><span class="p">],</span> <span class="ss">limit: </span><span class="mi">100</span><span class="p">}</span>
<span class="n">params</span><span class="p">[</span><span class="ss">:starting_after</span><span class="p">]</span> <span class="o">=</span> <span class="n">starting_after</span> <span class="k">if</span> <span class="n">starting_after</span>
<span class="n">customers</span> <span class="o">=</span> <span class="no">Stripe</span><span class="o">::</span><span class="no">Customer</span><span class="p">.</span><span class="nf">list</span><span class="p">(</span><span class="n">params</span><span class="p">)</span>
<span class="n">customers</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">cus</span><span class="o">|</span>
<span class="no">ApplicationRecord</span><span class="p">.</span><span class="nf">transaction</span> <span class="k">do</span>
<span class="no">StripeCustomer</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span>
<span class="ss">email: </span><span class="n">cus</span><span class="p">[</span><span class="s2">"email"</span><span class="p">],</span>
<span class="ss">stripe_id: </span><span class="n">cus</span><span class="p">[</span><span class="s2">"id"</span><span class="p">],</span>
<span class="ss">description: </span><span class="n">cus</span><span class="p">[</span><span class="s2">"description"</span><span class="p">],</span>
<span class="ss">created_in_stripe: </span><span class="n">cus</span><span class="p">[</span><span class="s2">"created"</span><span class="p">]</span> <span class="p">?</span> <span class="no">Time</span><span class="p">.</span><span class="nf">zone</span><span class="p">.</span><span class="nf">at</span><span class="p">(</span><span class="n">cus</span><span class="p">[</span><span class="s2">"created"</span><span class="p">])</span> <span class="p">:</span> <span class="kp">nil</span><span class="p">,</span>
<span class="ss">metadata: </span><span class="n">cus</span><span class="p">[</span><span class="s2">"metadata"</span><span class="p">],</span>
<span class="ss">subscriptions: </span><span class="n">customer_subscriptions</span><span class="p">,</span>
<span class="ss">deleted: </span><span class="n">cus</span><span class="p">[</span><span class="s2">"deleted"</span><span class="p">]</span>
<span class="p">)</span>
<span class="n">cus</span><span class="p">.</span><span class="nf">subscriptions</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="nb">sub</span><span class="o">|</span>
<span class="no">StripeSubscription</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span>
<span class="ss">stripe_customer_id: </span><span class="n">cus</span><span class="p">[</span><span class="s2">"id"</span><span class="p">],</span>
<span class="ss">stripe_id: </span><span class="nb">sub</span><span class="p">[</span><span class="s2">"id"</span><span class="p">],</span>
<span class="ss">plan_id: </span><span class="nb">sub</span><span class="p">[</span><span class="s2">"plan"</span><span class="p">][</span><span class="s2">"id"</span><span class="p">],</span>
<span class="ss">status: </span><span class="nb">sub</span><span class="p">[</span><span class="s2">"status"</span><span class="p">]</span>
<span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">starting_after</span> <span class="o">=</span> <span class="n">cus</span><span class="p">[</span><span class="s2">"id"</span><span class="p">]</span>
<span class="k">end</span>
<span class="k">break</span> <span class="k">if</span> <span class="n">customers</span><span class="p">.</span><span class="nf">count</span> <span class="o"><</span> <span class="mi">100</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">drop_old_data</span>
<span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">connection</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="o"><<~</span><span class="no">SQL</span><span class="p">)</span><span class="sh">
delete from stripe.customers;
delete from stripe.subscriptions;
</span><span class="no"> SQL</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>And this is the configuration defined in <code class="highlighter-rouge">config/sidekiq_scheduler.yml</code> to run it every Saturday at 12:00PM in UTC time zone:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">production</span><span class="pi">:</span>
<span class="na">update_stripe_data</span><span class="pi">:</span>
<span class="na">class</span><span class="pi">:</span> <span class="s">UpdateStripeData</span>
<span class="na">cron</span><span class="pi">:</span> <span class="s1">'</span><span class="s">0</span><span class="nv"> </span><span class="s">12</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">6'</span>
</code></pre></div></div>
<p>We chose that time as the servers are less loaded during those hours.</p>
<section style="background-color: rgba(240,67,56,.08); padding: 10px; padding-bottom: 20px; border-radius: 10px; margin-top: 20px; text-align: center; font-family: Arial, Helvetica, sans-serif;">
<strong class="seeking-assistance-magnete">Are you seeking assistance with Ruby on Rails development?</strong>
<div style="display: flex;align-items:center;justify-content: center;margin-top: 20px;">
<a class="schedule-button" style="background-color: #266cb4;" target="_blank" rel="nofollow" href="https://calendly.com/andrei-kaleshka/30min">Schedule a call</a>
</div>
</section>
<h2 id="analyze-stripe-data-discrepancies-with-sql">Analyze Stripe data discrepancies with SQL</h2>
<p>We are all set. With the data in place, we can use SQL to identify all deviations. This will involve using common table expression (CTE), views, joins, filters, and aggregation functions.</p>
<p>Note that the script fetches the Stripe data once a week, and it takes around 1 hour to run. Consequently, the app data will always be ahead of the Stripe data. This is crucial to understand because we cannot join the current app data against the cached Stripe data due to this time lag. Therefore, we need the app data snapshot at the moment the script was run.</p>
<p>Creating a real data snapshot is an option, but it would significantly complicate our architectural story. Fortunately, we have Papertrail configured in the app, a gem that stores all changes to users. This means we can reinstall actual data at the moment the script fetched Stripe data was run. The only field of interest for us is <code class="highlighter-rouge">plan_id</code>. Therefore, we reinstall only this field. To reuse the reinstalled data, we utilize views. These are kind of virtual tables inside SQL but don’t store the collected data:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">create</span> <span class="k">view</span> <span class="n">stripe</span><span class="p">.</span><span class="n">users_with_actual_plan</span> <span class="k">as</span> <span class="p">(</span>
<span class="k">with</span> <span class="n">stripe_update</span> <span class="k">as</span> <span class="p">(</span>
<span class="k">select</span> <span class="n">created_at</span> <span class="k">as</span> <span class="n">ts</span> <span class="k">from</span> <span class="n">stripe</span><span class="p">.</span><span class="n">subscriptions</span> <span class="k">limit</span> <span class="mi">1</span>
<span class="p">),</span>
<span class="n">recent_versions</span> <span class="k">as</span> <span class="p">(</span>
<span class="k">select</span> <span class="o">*</span> <span class="k">from</span> <span class="n">versions</span> <span class="k">where</span> <span class="n">created_at</span> <span class="o">>=</span> <span class="p">(</span><span class="k">select</span> <span class="n">ts</span> <span class="o">-</span> <span class="n">interval</span> <span class="s1">'1 day'</span> <span class="k">from</span> <span class="n">stripe_update</span><span class="p">)</span>
<span class="p">),</span>
<span class="k">data</span> <span class="k">as</span> <span class="p">(</span>
<span class="k">select</span>
<span class="n">u</span><span class="p">.</span><span class="o">*</span><span class="p">,</span>
<span class="n">coalesce</span> <span class="p">(</span>
<span class="n">coalesce</span><span class="p">(</span>
<span class="n">coalesce</span><span class="p">(</span>
<span class="p">(</span><span class="k">substring</span><span class="p">((</span><span class="n">vp</span><span class="p">.</span><span class="n">object_changes</span><span class="o">->></span><span class="s1">'plan_id'</span><span class="p">)</span> <span class="k">from</span> <span class="s1">', (</span><span class="se">\d</span><span class="s1">+)'</span><span class="p">))::</span><span class="nb">int</span><span class="p">,</span>
<span class="p">(</span><span class="k">substring</span><span class="p">(</span><span class="n">vp</span><span class="p">.</span><span class="k">object</span> <span class="k">from</span> <span class="s1">'</span><span class="se">\n</span><span class="s1">plan_id: (</span><span class="se">\d</span><span class="s1">+)'</span><span class="p">))::</span><span class="nb">int</span>
<span class="p">),</span>
<span class="n">coalesce</span><span class="p">(</span>
<span class="p">(</span><span class="k">substring</span><span class="p">((</span><span class="n">vf</span><span class="p">.</span><span class="n">object_changes</span><span class="o">->></span><span class="s1">'plan_id'</span><span class="p">)</span> <span class="k">from</span> <span class="s1">'(</span><span class="se">\d</span><span class="s1">+),'</span><span class="p">))::</span><span class="nb">int</span><span class="p">,</span>
<span class="p">(</span><span class="k">substring</span><span class="p">(</span><span class="n">vf</span><span class="p">.</span><span class="k">object</span> <span class="k">from</span> <span class="s1">'</span><span class="se">\n</span><span class="s1">plan_id: (</span><span class="se">\d</span><span class="s1">+)'</span><span class="p">))::</span><span class="nb">int</span>
<span class="p">)</span>
<span class="p">),</span>
<span class="n">u</span><span class="p">.</span><span class="n">plan_id</span>
<span class="p">)</span> <span class="k">as</span> <span class="n">actual_plan_id</span><span class="p">,</span>
<span class="n">rank</span><span class="p">()</span> <span class="n">over</span> <span class="p">(</span><span class="n">partition</span> <span class="k">by</span> <span class="n">u</span><span class="p">.</span><span class="n">id</span> <span class="k">order</span> <span class="k">by</span> <span class="n">vp</span><span class="p">.</span><span class="n">created_at</span> <span class="k">desc</span> <span class="n">nulls</span> <span class="k">last</span><span class="p">,</span> <span class="n">vf</span><span class="p">.</span><span class="n">created_at</span> <span class="k">asc</span><span class="p">)</span> <span class="k">as</span> <span class="n">version_rank</span>
<span class="k">from</span> <span class="n">users</span> <span class="n">u</span>
<span class="k">left</span> <span class="k">join</span> <span class="n">stripe</span><span class="p">.</span><span class="n">customers</span> <span class="k">c</span> <span class="k">on</span> <span class="k">c</span><span class="p">.</span><span class="n">stripe_id</span> <span class="o">=</span> <span class="n">u</span><span class="p">.</span><span class="n">stripe_customer_id</span>
<span class="k">left</span> <span class="k">join</span> <span class="n">recent_versions</span> <span class="n">vp</span> <span class="k">on</span> <span class="n">vp</span><span class="p">.</span><span class="n">item_id</span> <span class="o">=</span> <span class="n">u</span><span class="p">.</span><span class="n">id</span> <span class="k">and</span> <span class="n">vp</span><span class="p">.</span><span class="n">created_at</span> <span class="o"><</span> <span class="n">coalesce</span><span class="p">(</span><span class="k">c</span><span class="p">.</span><span class="n">created_at</span><span class="p">,</span> <span class="p">(</span><span class="k">select</span> <span class="n">ts</span> <span class="k">from</span> <span class="n">stripe_update</span><span class="p">))</span>
<span class="k">left</span> <span class="k">join</span> <span class="n">recent_versions</span> <span class="n">vf</span> <span class="k">on</span> <span class="n">vf</span><span class="p">.</span><span class="n">item_id</span> <span class="o">=</span> <span class="n">u</span><span class="p">.</span><span class="n">id</span> <span class="k">and</span> <span class="n">vf</span><span class="p">.</span><span class="n">created_at</span> <span class="o">>=</span> <span class="n">coalesce</span><span class="p">(</span><span class="k">c</span><span class="p">.</span><span class="n">created_at</span><span class="p">,</span> <span class="p">(</span><span class="k">select</span> <span class="n">ts</span> <span class="k">from</span> <span class="n">stripe_update</span><span class="p">))</span>
<span class="p">)</span> <span class="k">select</span> <span class="o">*</span> <span class="k">from</span> <span class="k">data</span> <span class="k">where</span> <span class="n">version_rank</span> <span class="o">=</span> <span class="mi">1</span>
<span class="p">);</span>
</code></pre></div></div>
<p>This SQL query might look intimidating, and that’s okay. It’s not essential for understanding everything here. All you need to know is what it does, and you already have that knowledge. It creates a virtual table of users with the actual <code class="highlighter-rouge">plan_id</code> that was set just before the script fetched Stripe data.</p>
<blockquote>
<p>One might ask, “How do we end up creating SQL queries that appear so complicated and intimidating?” The answer is simple: through small steps, selecting one field at a time, joining step by step, and eventually combining them into a single query using the CTE construction.</p>
</blockquote>
<p>And now, we are ready to join these users with the actual plan data from Stripe.</p>
<p>This time, the SQL looks much easier:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">with</span> <span class="n">stripe_update</span> <span class="k">as</span> <span class="p">(</span>
<span class="k">select</span> <span class="n">created_at</span> <span class="n">ts</span> <span class="k">from</span> <span class="n">stripe</span><span class="p">.</span><span class="n">subscriptions</span> <span class="k">limit</span> <span class="mi">1</span>
<span class="p">)</span>
<span class="k">select</span> <span class="n">u</span><span class="p">.</span><span class="o">*</span> <span class="k">from</span>
<span class="n">stripe</span><span class="p">.</span><span class="n">users_with_actual_plan</span> <span class="n">u</span>
<span class="k">left</span> <span class="k">join</span> <span class="n">stripe</span><span class="p">.</span><span class="n">subscriptions</span> <span class="n">s</span> <span class="k">on</span> <span class="n">s</span><span class="p">.</span><span class="n">stripe_customer_id</span> <span class="o">=</span> <span class="n">u</span><span class="p">.</span><span class="n">stripe_customer_id</span>
<span class="k">where</span> <span class="n">u</span><span class="p">.</span><span class="n">actual_plan_id</span> <span class="k">is</span> <span class="k">not</span> <span class="k">null</span> <span class="k">and</span> <span class="n">u</span><span class="p">.</span><span class="n">actual_plan_id</span> <span class="k">not</span> <span class="k">in</span> <span class="p">(</span><span class="mi">6</span><span class="p">,</span> <span class="mi">121</span><span class="p">)</span>
<span class="k">and</span> <span class="n">u</span><span class="p">.</span><span class="n">created_at</span> <span class="o"><</span> <span class="p">(</span><span class="k">select</span> <span class="n">ts</span> <span class="k">from</span> <span class="n">stripe_update</span><span class="p">)</span>
<span class="k">and</span> <span class="n">s</span><span class="p">.</span><span class="n">id</span> <span class="k">is</span> <span class="k">null</span>
<span class="p">;</span>
</code></pre></div></div>
<p>The plan with ID = 6 is the free plan, and 121 is a technical one used only for internal purposes. Therefore, the entire expression <code class="highlighter-rouge">u.actual_plan_id is not null and u.actual_plan_id not in (6, 121)</code> indicates that the user is on a paid plan.</p>
<p>We inputted this SQL into Metabase, and this is what it looks like:</p>
<p><img src="/images/reported-users.png" alt="SQL inside Metabase" /></p>
<blockquote>
<p>Optionally, we can configure notifications to be sent even when a new record is added to these results. See the bell icon at the bottom right; it’s intended for this purpose.</p>
</blockquote>
<p>Metabase comes in handy for exporting data into CSV and Excel with just one click. We do that often. That simplifies communication with the business a lot. It’s also possible to share the reports right away with the team via a direct link.</p>
<p>As we can see, there were 983 instances of user data discrepancies found. It’s a significant amount of data among all active subscriptions that match the app user data, which is 8,488. That represents roughly a 10% margin of error! Too much!</p>
<p>We found the number of subscriptions that match user data using this SQL:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">with</span> <span class="n">stripe_update</span> <span class="k">as</span> <span class="p">(</span>
<span class="k">select</span> <span class="n">created_at</span> <span class="n">ts</span> <span class="k">from</span> <span class="n">stripe</span><span class="p">.</span><span class="n">subscriptions</span> <span class="k">limit</span> <span class="mi">1</span>
<span class="p">)</span>
<span class="k">select</span> <span class="k">count</span><span class="p">(</span><span class="n">u</span><span class="p">.</span><span class="o">*</span><span class="p">)</span> <span class="k">from</span> <span class="n">stripe</span><span class="p">.</span><span class="n">users_with_actual_plan</span> <span class="n">u</span>
<span class="k">join</span> <span class="n">stripe</span><span class="p">.</span><span class="n">subscriptions</span> <span class="n">s</span> <span class="k">on</span> <span class="n">s</span><span class="p">.</span><span class="n">stripe_customer_id</span> <span class="o">=</span> <span class="n">u</span><span class="p">.</span><span class="n">stripe_customer_id</span>
<span class="k">join</span> <span class="n">plans</span> <span class="n">p</span> <span class="k">on</span> <span class="n">p</span><span class="p">.</span><span class="n">stripe_price_id</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">plan_id</span>
<span class="k">where</span> <span class="n">u</span><span class="p">.</span><span class="n">created_at</span> <span class="o"><</span> <span class="p">(</span><span class="k">select</span> <span class="n">ts</span> <span class="k">from</span> <span class="n">stripe_update</span><span class="p">)</span>
<span class="p">;</span>
</code></pre></div></div>
<blockquote>
<p>See how the created view “users_with_actual_plan” becomes handy here as well. It’s a very powerful tool that can save a ton of code and coding time!</p>
</blockquote>
<p>Further data analysis shows that 514 of these 983 discrepancies are related to deleted users. It turns out our app employs a so-called soft delete feature, meaning they are not actually deleted but marked as such, preventing them from using the app anymore. Well, now the situation looks much better. It’s just a 5% margin of error, not the 10% we initially thought. It’s almost within 3% of the standard deviation. Not so bad.</p>
<section style="background-color: rgba(240,67,56,.08); padding: 10px; padding-bottom: 20px; border-radius: 10px; margin-top: 20px; text-align: center; font-family: Arial, Helvetica, sans-serif;">
<strong class="seeking-assistance-magnete">Are you seeking assistance with Ruby on Rails development?</strong>
<div style="display: flex;align-items:center;justify-content: center;margin-top: 20px;">
<a class="schedule-button" style="background-color: #266cb4;" target="_blank" rel="nofollow" href="https://calendly.com/andrei-kaleshka/30min">Schedule a call</a>
</div>
</section>
<h2 id="fixing-the-data-deviations-between-the-app-and-stripe">Fixing the data deviations between the app and Stripe</h2>
<p>All actions related to fixing data require a thorough understanding of the app at a good level. We can either examine the instances of discrepancies one by one, allowing us to understand what happened to each of them. When we analyze each instance of discrepancy, we check the app database, logs, Papertrail records, Stripe logs, Rollbar, and any other data sources that could help us determine what happened to a certain user.</p>
<p>By simply looking into the app database, we noticed those 514 softly deleted users. This can be easily done by examining the <code class="highlighter-rouge">deleted_at</code> column in the report. Any Excel-like tool can allow us to filter this information, or we can accomplish this in Metabase.</p>
<p><img src="/images/metabase-filters.png" alt="Filter by non-null deleted_at column in Metabase" /></p>
<p>We can move these users to free plan right away by writing a Ruby script that was successfully done.</p>
<p>Now, there are only 469 records with data discrepancies remaining. We go through some of them. That shows that all of them indeed had data discrepancies. Delving into the codebase with some hypotheses about what happened, we spot a serious breach inside the app. Each Stripe webhook has its own handler with specific code manipulating the user subscription data. All processed by Sidekiq. But Sidekiq doesn’t guarantee the order of incoming webhook processing.</p>
<p>At this point, we decide to address this issue by making slight adjustments to the architecture. From now on, all webhooks will be processed by one handler that consistently checks the actual Stripe subscription state before changing the user’s plan within the app. Hence, the multiple webhooks that used to process subscription-related actions will now utilize only one blocking handler, ensuring it cannot run in parallel for several requests for one user.</p>
<p>This is the simplicied version of the code we used to fix the bug:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">sync_app_subscription</span>
<span class="no">PlanChangeSchedule</span><span class="p">.</span><span class="nf">transaction</span> <span class="k">do</span>
<span class="c1"># Prevent concurrent plan updates; if another job attempts to acquire the lock simultaneously, it will wait until this one is completed</span>
<span class="no">PlanChangeSchedule</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">user: </span><span class="n">user</span><span class="p">).</span><span class="nf">lock</span>
<span class="k">if</span> <span class="n">actual_subscription</span> <span class="o">&&</span> <span class="n">stripe_plan</span> <span class="o">!=</span> <span class="n">user</span><span class="p">.</span><span class="nf">plan</span>
<span class="n">synchronize!</span>
<span class="k">elsif</span> <span class="o">!</span><span class="n">actual_subscription</span>
<span class="n">cancel_account</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">ensure</span>
<span class="no">PlanChangeSchedule</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">user: </span><span class="n">user</span><span class="p">).</span><span class="nf">delete_all</span>
<span class="k">end</span>
</code></pre></div></div>
<p><code class="highlighter-rouge">PlanChangeSchedule</code> is a special table that stores the user’s plan change requests. It’s used to prevent concurrent plan updates. The <code class="highlighter-rouge">lock</code> method is used to prevent concurrent plan updates. If another job attempts to acquire the lock simultaneously, it will wait until this one is completed. The <code class="highlighter-rouge">synchronize!</code> method is used to synchronize the user’s plan with the actual Stripe subscription. The <code class="highlighter-rouge">cancel_account</code> method is used to cancel the user’s account if there is no actual subscription.</p>
<section style="background-color: rgba(240,67,56,.08); padding: 10px; padding-bottom: 20px; border-radius: 10px; margin-top: 20px; text-align: center; font-family: Arial, Helvetica, sans-serif;">
<strong class="seeking-assistance-magnete">Are you seeking assistance with Ruby on Rails development?</strong>
<div style="display: flex;align-items:center;justify-content: center;margin-top: 20px;">
<a class="schedule-button" style="background-color: #266cb4;" target="_blank" rel="nofollow" href="https://calendly.com/andrei-kaleshka/30min">Schedule a call</a>
</div>
</section>
<p>After fixing the bug, we transferred all users who had not used the app in the last 90 days to the free plan. There were 391 such accounts. The remaining users got moved to the paid subscription. In the end, the result looked like that:</p>
<ul>
<li>30 users got moved to the free plan;</li>
<li>For the rest 48, we tried to reactivate the subscription. 19 of which got successfully reactivated. 5 of them, could not have created the subscription due to a missing payment method. 24 of them created subscriptions but with the payment failing, they will be moved to the free plan after 3 failed attempts, or remain active if paid.</li>
</ul>
<p>That resulted in roughly a $525 immediate increase in Monthly Recurring Revenue (MRR) and 600$ in potential MRR increase if all the users with failed payments will be reactivated.</p>
<h2 id="preventing-future-discrepancies">Preventing future discrepancies</h2>
<p>To prevent future discrepancies, we set up a monitoring system that checks for new discrepancies every week. We use Metabase for this purpose. We set up a dashboard that shows the number of discrepancies. We also set up alerts that notify us when the number of discrepancies exceeds a certain threshold. All of that was done using Metabase and SQL.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Asynchronous communication between services is a painful point. It can lead to customer dissatisfaction, losses to the business, and increased workload for the customer support team. This post shows how we deal with these kinds of issues. Efficiently solving them requires deep expertise in SQL, the business, the tech stack, data analysis, and programming.</p>
<p>Happy coding and bug-free apps to you, our reader!</p>
<p><a href="http://blog.widefix.com/reconcile-app-users-against-stripe-and-prevent-financial-losses/">Reconcile app users vs Stripe and prevent financial losses</a> was originally published by Andrei Kaleshka at <a href="http://blog.widefix.com">Take your Rails development to the next level</a> on January 30, 2024.</p>http://blog.widefix.com/smart-route-aliases-in-rails2023-12-19 18:00:50 +0100T00:00:00-00:002023-12-19T00:00:00-05:00Andrei Kaleshkahttp://blog.widefix.comka8725@gmail.com
<p>Sometimes, for SEO and marketing reasons, you might need to create an alias route in your Rails project. This can get tricky if the original route relies on dynamic elements that change based on the user’s session.</p>
<p>In our project, we wanted to create a quick and easy shortcut, <code class="highlighter-rouge">/edit_my_plans</code>, for the user-specific route <code class="highlighter-rouge">/users/:id/edit?tab=plans</code>.</p>
<p>For example, Rails offers a way to set up this type of redirect using the following code (taken from their official documentation):</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">get</span> <span class="s1">'/stories/:name'</span><span class="p">,</span> <span class="ss">to: </span><span class="n">redirect</span> <span class="p">{</span> <span class="o">|</span><span class="n">path_params</span><span class="p">,</span> <span class="n">req</span><span class="o">|</span> <span class="s2">"/articles/</span><span class="si">#{</span><span class="n">path_params</span><span class="p">[</span><span class="ss">:name</span><span class="p">].</span><span class="nf">pluralize</span><span class="si">}</span><span class="s2">"</span> <span class="p">}</span>
</code></pre></div></div>
<p>With this knowledge in hand, we developed a clear and straightforward method for defining these types of routes, allowing us to embed business logic of any level of complexity directly into the router:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># in config/routes.rb:</span>
<span class="n">get</span> <span class="s2">"/edit_my_plans"</span><span class="p">,</span> <span class="ss">to: </span><span class="n">redirect</span><span class="p">(</span><span class="no">RoutesAlias</span><span class="p">.</span><span class="nf">edit_my_plans</span><span class="p">)</span>
</code></pre></div></div>
<p>This is our special class, called <code class="highlighter-rouge">RoutesAlias</code>, that handles all the complex stuff. It’s better to keep this stuff separate from the router, so we put it in the <code class="highlighter-rouge">app/lib</code> folder. You can put it somewhere else in your project if you want.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">RoutesAlias</span>
<span class="kp">include</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">url_helpers</span>
<span class="nb">attr_reader</span> <span class="ss">:session</span>
<span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">edit_my_plans</span>
<span class="nb">proc</span> <span class="p">{</span> <span class="o">|</span><span class="n">_params</span><span class="p">,</span> <span class="n">request</span><span class="o">|</span> <span class="n">new</span><span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="nf">session</span><span class="p">).</span><span class="nf">edit_my_plans</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">session</span><span class="p">)</span>
<span class="vi">@session</span> <span class="o">=</span> <span class="n">session</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">edit_my_plans</span>
<span class="n">current_user</span> <span class="o">=</span> <span class="no">Authentication</span><span class="p">.</span><span class="nf">user_from_session</span><span class="p">(</span><span class="n">session</span><span class="p">)</span>
<span class="k">case</span> <span class="n">current_user</span><span class="o">&</span><span class="p">.</span><span class="nf">user_type</span>
<span class="k">when</span> <span class="s2">"vendor"</span>
<span class="n">edit_user_path</span><span class="p">(</span><span class="n">current_user</span><span class="p">,</span> <span class="ss">tab: </span><span class="s2">"plans"</span><span class="p">)</span>
<span class="k">when</span> <span class="s2">"user"</span>
<span class="n">projects_path</span>
<span class="k">when</span> <span class="kp">nil</span>
<span class="s2">"/auth/auth0?origin=/edit_my_plans"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Now, marketing sites, like landing pages or other marketing pages, can easily use the generic URL <code class="highlighter-rouge">/edit_my_plans</code>. Rails will handle everything, taking you to the right place, whether it’s a login form, a specific homepage for a particular user type, or something else.</p>
<p>Hope this helps with your Rails project! Happy coding!</p>
<p><a href="http://blog.widefix.com/smart-route-aliases-in-rails/">Smart route aliases in Rails</a> was originally published by Andrei Kaleshka at <a href="http://blog.widefix.com">Take your Rails development to the next level</a> on December 19, 2023.</p>http://blog.widefix.com/risk-free-redesign-ruby-on-rails-app2023-08-30 15:06:43 +0200T00:00:00-00:002023-08-30T00:00:00-04:00Andrei Kaleshkahttp://blog.widefix.comka8725@gmail.com
<h2 id="ruby-on-rails-app-redesigning-challenges">Ruby on Rails app redesigning challenges</h2>
<p>Redesigning a Ruby on Rails application is a well-known challenge for many projects. Any project UI gets outdated. It needs to get a fresh look that’s more appealing to the users. It may not be an issue for web apps without live users. But it’s a tricky task for already launched businesses serving thousands of users per day.</p>
<p>While a development team is redesigning, the application should continue functioning. At the development time, the new design gets validated and tested against the back-end. If the back-end is incompatible, it must be adapted to the new realm. These changes should not break the old functionality. Overcoming these issues is essential in a Ruby on Rails application redesign.</p>
<p><img src="/images/redesign-decision.jpg" alt="Ruby On Rails redesign decision" /></p>
<p>This article shares the approach we took to apply the new design in one of our projects. The changes we made increased the project revenue by 30%. As a bonus, the implemented changes unleashed many other opportunities, including:</p>
<ul>
<li>Building a modern mobile application.</li>
<li>Making the code more fault-tolerant, performant, and stable.</li>
<li>Adding new features, such as preventing account sharing, that can increase the app revenue.</li>
<li>Making the tech stack upgrade easier.</li>
<li>Implementing traffic control and advanced caching.</li>
<li>Making SEO optimization easier.</li>
</ul>
<section style="background-color: rgba(240,67,56,.08); padding: 10px; padding-bottom: 20px; border-radius: 10px; margin-top: 20px; text-align: center; font-family: Arial, Helvetica, sans-serif;">
<strong class="seeking-assistance-magnete">Are you seeking assistance with Ruby on Rails development?</strong>
<div style="display: flex;align-items:center;justify-content: center;margin-top: 20px;">
<a class="schedule-button" style="background-color: #266cb4;" target="_blank" rel="nofollow" href="https://calendly.com/andrei-kaleshka/30min">Schedule a call</a>
</div>
</section>
<h2 id="why-redesigning-a-ruby-on-rails-app">Why redesigning a Ruby on Rails app</h2>
<p>There are several reasons why an app may consider implementing a new UI/UX design. Regardless of the motive, the ultimate goal is always the same - <strong>to increase revenue</strong>. Note, that keeping users using the app and not going away is the same goal. Due to an old and unhandy design, an app can make the customers leave. In this case, redesign also can help. If it’s clear that a redesign won’t have a positive impact on revenue, it may be an unnecessary expense. Redesigning is usually an expensive change. So learn from the users if the design is a problem before making this decision.</p>
<p><img src="/images/redesign-weigh.jpg" alt="Ruby On Rails redesign weigh" /></p>
<h2 id="what-is-the-new-design">What is the new design</h2>
<p>A new design consists of screens (mockups) created by a designer using software like Figma or Adobe Photoshop.</p>
<p>In our case, the client provided the mockups, and our task was to turn them into code and integrate them with the existing back-end. We aimed to minimize downtime during the transition and reduce development efforts.</p>
<p><img src="/images/redesign-new-design.jpg" alt="What is the new design" /></p>
<h2 id="different-technical-approaches-of-a-ruby-on-rails-app-redesign">Different technical approaches of a Ruby on Rails app redesign</h2>
<p>Nowadays, web projects must be responsive and have a mobile application. Modern web app design features a rich UI with many elements on one page. The old-fashioned way of generating HTML on the back-end is going away. Instead, a separate web front-end or mobile app handles the front-end, while the back-end serves data via API.</p>
<p>In pure Ruby On Rails applications, the front-end code lives alongside the back-end. That can fulfill modern web app requirements and is cheap in the beginning. However, it becomes hard to maintain due to the mix of different technologies in one place. It makes it difficult to find developers who can understand and maintain the system. We prefer separate back-end and front-end. The specialists can do their job quickly with high quality on their end.</p>
<p><img src="/images/redesign-business.jpg" alt="Redesign Ruby On Rails business" /></p>
<h2 id="risk-free-approach-of-a-ruby-on-rails-app-redesign">Risk-free approach of a Ruby on Rails app redesign</h2>
<p>The project we received was a Ruby On Rails application with a Rest API implemented on Grape. An iOS app used this API. The web version used ERB and Slim templates with a UI from the previous decade. Some dynamic features on the web used Knockout.js, which is no longer maintained.</p>
<p>To update the UI, we used the Next.js framework with TypeScript and implemented GraphQL for the API. To avoid errors, we created a separate repository for the new UI and extracted functionality that could be reused in both the old app and the new UI.</p>
<p>The project took almost a year with 2 developers and 1 project manager working on it. To deploy the changes, we used CloudFront as a proxy on top of the old app and the new front-end app. We gradually switched web requests to the new UI using a feature flag to reduce the risk of failures or outages. The transition went smoothly without major issues.</p>
<p><img src="/images/rails-app-redesign.png" alt="Risk free Rails App Redesign" /></p>
<p>As a bonus, we got all requests geolocated since they pass through the AWS CloudFront. That allowed us to control the users’ network traffic and fight against sharing accounts.</p>
<h2 id="our-results-of-the-ruby-on-rails-app-redesign">Our results of the Ruby on Rails app redesign</h2>
<p>Switching to the new design made the project more attractive to users, resulting in more signups and increased satisfaction among old users. This allowed us to increase charges by 20% and revenue by 30%. The redesign expenses were paid off within the first 3 months after release. See below for the paid user dynamics analysis.</p>
<p><img src="/images/users-increase.png" alt="User signups dynamics" /></p>
<p>The release date was on the 1st of February 2021.</p>
<section style="background-color: rgba(240,67,56,.08); padding: 10px; padding-bottom: 20px; border-radius: 10px; margin-top: 20px; text-align: center; font-family: Arial, Helvetica, sans-serif;">
<strong class="seeking-assistance-magnete">Are you seeking assistance with Ruby on Rails development?</strong>
<div style="display: flex;align-items:center;justify-content: center;margin-top: 20px;">
<a class="schedule-button" style="background-color: #266cb4;" target="_blank" rel="nofollow" href="https://calendly.com/andrei-kaleshka/30min">Schedule a call</a>
</div>
</section>
<h2 id="why-our-approach-of-ruby-on-rails-app-redesign-is-cheaper-and-less-risky">Why our approach of Ruby on Rails app redesign is cheaper and less risky</h2>
<p>Redesigning apps can be painful and distracting. The Rails assets pipeline changes often, making sticking with it turn the code into legacy quickly. Finding specialists to maintain this code without sacrificing quality on both front-end and back-end is challenging. Separating the front-end and leaving only the Rails back-end for the API makes redesigning and maintenance smoother, allowing for a diversified team with good specialists on both ends.</p>
<p>Next.js was a wise choice for the front-end, improving SEO with image optimization and cache facilities out of the box.</p>
<p><img src="/images/redesign-seo.png" alt="Redesign Rails App SEO impact" /></p>
<h2 id="acknowledges">Acknowledges</h2>
<p>I’m grateful and proud to have worked with the following people on this project at different times:</p>
<ul>
<li>Daniel Dauwe</li>
<li>Vadzim Jakushau</li>
<li>Illia Pruskyi</li>
<li>Soltan Yangibayev</li>
<li>Svetlana Zhuravitskaya</li>
<li>Alexey Mikitsik</li>
</ul>
<p><small class="side-note">
When seeking proficient expertise in Ruby on Rails development, explore the option of using a reputable talent marketplace. Toptal provides access to dependable <a href="https://www.toptal.com/ruby-on-rails">Ruby on Rails developers</a> for hire, assisting companies with their critical web application requirements.
</small></p>
<p><a href="http://blog.widefix.com/risk-free-redesign-ruby-on-rails-app/">Cheaper and Risk-Free Ruby on Rails App Redesign</a> was originally published by Andrei Kaleshka at <a href="http://blog.widefix.com">Take your Rails development to the next level</a> on August 30, 2023.</p>http://blog.widefix.com/select-unique-latest-grouped-records-from-db2023-07-13 17:26:54 +0200T00:00:00-00:002023-07-13T00:00:00-04:00Andrei Kaleshkahttp://blog.widefix.comka8725@gmail.com
<p>Nowadays, almost every Ruby on Rails application has a so-called recent records block.
This block usually shows statistics or a list of recent things within the project during the last few days searched by some criteria.
It can be something like “top 10 products”, “the most popular projects”, or “the most relevant apartments”. Read this blog post the learn how to efficiently build data for these blocks using SQL and window functions in Ruby on Rails app.</p>
<h2 id="recent-records---the-task-overview">Recent records - the task overview</h2>
<p>Assume that you’ve got an app that has <code class="highlighter-rouge">Project</code> model. It has many <code class="highlighter-rouge">ratings</code>. The <code class="highlighter-rouge">Rating</code> model has just a value from 1 to 5 assigned by users to some projects.</p>
<p>For the task understanding it’s enough to have a look into the table definition:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">db</span><span class="o">-#</span> <span class="err">\</span><span class="n">d</span> <span class="n">ratings</span>
<span class="k">Table</span> <span class="nv">"public.ratings"</span>
<span class="k">Column</span> <span class="err">│</span> <span class="k">Type</span> <span class="err">│</span> <span class="k">Collation</span> <span class="err">│</span> <span class="k">Nullable</span> <span class="err">│</span> <span class="k">Default</span>
<span class="err">═════════════╪════════════════════════════════╪═══════════╪══════════╪═════════════════════════════════════</span>
<span class="n">id</span> <span class="err">│</span> <span class="nb">bigint</span> <span class="err">│</span> <span class="err">│</span> <span class="k">not</span> <span class="k">null</span> <span class="err">│</span> <span class="n">nextval</span><span class="p">(</span><span class="s1">'ratings_id_seq'</span><span class="p">::</span><span class="n">regclass</span><span class="p">)</span>
<span class="n">reviewer_id</span> <span class="err">│</span> <span class="nb">bigint</span> <span class="err">│</span> <span class="err">│</span> <span class="err">│</span>
<span class="n">reviewee_id</span> <span class="err">│</span> <span class="nb">bigint</span> <span class="err">│</span> <span class="err">│</span> <span class="err">│</span>
<span class="n">rating</span> <span class="err">│</span> <span class="nb">integer</span> <span class="err">│</span> <span class="err">│</span> <span class="err">│</span>
<span class="n">review</span> <span class="err">│</span> <span class="nb">text</span> <span class="err">│</span> <span class="err">│</span> <span class="err">│</span>
<span class="n">project_id</span> <span class="err">│</span> <span class="nb">bigint</span> <span class="err">│</span> <span class="err">│</span> <span class="err">│</span>
<span class="n">created_at</span> <span class="err">│</span> <span class="nb">timestamp</span><span class="p">(</span><span class="mi">6</span><span class="p">)</span> <span class="k">without</span> <span class="nb">time</span> <span class="k">zone</span> <span class="err">│</span> <span class="err">│</span> <span class="k">not</span> <span class="k">null</span> <span class="err">│</span>
<span class="n">updated_at</span> <span class="err">│</span> <span class="nb">timestamp</span><span class="p">(</span><span class="mi">6</span><span class="p">)</span> <span class="k">without</span> <span class="nb">time</span> <span class="k">zone</span> <span class="err">│</span> <span class="err">│</span> <span class="k">not</span> <span class="k">null</span> <span class="err">│</span>
<span class="n">Indexes</span><span class="p">:</span>
<span class="nv">"ratings_pkey"</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span> <span class="n">btree</span> <span class="p">(</span><span class="n">id</span><span class="p">)</span>
</code></pre></div></div>
<p>This table has the following data:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">db</span><span class="o">-#</span> <span class="k">select</span> <span class="n">id</span><span class="p">,</span> <span class="n">reviewer_id</span><span class="p">,</span> <span class="n">reviewee_id</span><span class="p">,</span> <span class="n">rating</span><span class="p">,</span> <span class="n">project_id</span><span class="p">,</span> <span class="n">created_at</span> <span class="k">from</span> <span class="n">ratings</span><span class="p">;</span>
<span class="n">id</span> <span class="err">│</span> <span class="n">reviewer_id</span> <span class="err">│</span> <span class="n">reviewee_id</span> <span class="err">│</span> <span class="n">rating</span> <span class="err">│</span> <span class="n">project_id</span> <span class="err">│</span> <span class="n">created_at</span>
<span class="err">════╪═════════════╪═════════════╪════════╪════════════╪════════════════════════════</span>
<span class="mi">8</span> <span class="err">│</span> <span class="mi">9</span> <span class="err">│</span> <span class="mi">10</span> <span class="err">│</span> <span class="mi">5</span> <span class="err">│</span> <span class="mi">27</span> <span class="err">│</span> <span class="mi">2022</span><span class="o">-</span><span class="mi">12</span><span class="o">-</span><span class="mi">05</span> <span class="mi">21</span><span class="p">:</span><span class="mi">46</span><span class="p">:</span><span class="mi">01</span><span class="p">.</span><span class="mi">583185</span>
<span class="mi">12</span> <span class="err">│</span> <span class="mi">7</span> <span class="err">│</span> <span class="mi">6</span> <span class="err">│</span> <span class="mi">5</span> <span class="err">│</span> <span class="mi">26</span> <span class="err">│</span> <span class="mi">2022</span><span class="o">-</span><span class="mi">12</span><span class="o">-</span><span class="mi">23</span> <span class="mi">14</span><span class="p">:</span><span class="mi">35</span><span class="p">:</span><span class="mi">11</span><span class="p">.</span><span class="mi">047002</span>
<span class="mi">13</span> <span class="err">│</span> <span class="mi">6</span> <span class="err">│</span> <span class="mi">7</span> <span class="err">│</span> <span class="mi">5</span> <span class="err">│</span> <span class="mi">26</span> <span class="err">│</span> <span class="mi">2022</span><span class="o">-</span><span class="mi">12</span><span class="o">-</span><span class="mi">23</span> <span class="mi">14</span><span class="p">:</span><span class="mi">36</span><span class="p">:</span><span class="mi">48</span><span class="p">.</span><span class="mi">366411</span>
<span class="mi">18</span> <span class="err">│</span> <span class="mi">9</span> <span class="err">│</span> <span class="mi">10</span> <span class="err">│</span> <span class="mi">5</span> <span class="err">│</span> <span class="mi">39</span> <span class="err">│</span> <span class="mi">2023</span><span class="o">-</span><span class="mi">03</span><span class="o">-</span><span class="mi">01</span> <span class="mi">23</span><span class="p">:</span><span class="mi">27</span><span class="p">:</span><span class="mi">52</span><span class="p">.</span><span class="mi">68548</span>
<span class="mi">19</span> <span class="err">│</span> <span class="mi">10</span> <span class="err">│</span> <span class="mi">9</span> <span class="err">│</span> <span class="mi">5</span> <span class="err">│</span> <span class="mi">39</span> <span class="err">│</span> <span class="mi">2023</span><span class="o">-</span><span class="mi">03</span><span class="o">-</span><span class="mi">01</span> <span class="mi">23</span><span class="p">:</span><span class="mi">28</span><span class="p">:</span><span class="mi">32</span><span class="p">.</span><span class="mi">880234</span>
<span class="mi">20</span> <span class="err">│</span> <span class="mi">9</span> <span class="err">│</span> <span class="mi">10</span> <span class="err">│</span> <span class="mi">5</span> <span class="err">│</span> <span class="mi">86</span> <span class="err">│</span> <span class="mi">2023</span><span class="o">-</span><span class="mi">03</span><span class="o">-</span><span class="mi">01</span> <span class="mi">23</span><span class="p">:</span><span class="mi">35</span><span class="p">:</span><span class="mi">15</span><span class="p">.</span><span class="mi">564763</span>
<span class="p">(</span><span class="mi">6</span> <span class="k">rows</span><span class="p">)</span>
</code></pre></div></div>
<p>Our task is to return the latest reviews per project. So the resulting records should be these:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="n">id</span> <span class="err">│</span> <span class="n">reviewer_id</span> <span class="err">│</span> <span class="n">reviewee_id</span> <span class="err">│</span> <span class="n">rating</span> <span class="err">│</span> <span class="n">project_id</span> <span class="err">│</span> <span class="n">created_at</span>
<span class="err">════╪═════════════╪═════════════╪════════╪════════════╪════════════════════════════</span>
<span class="mi">8</span> <span class="err">│</span> <span class="mi">9</span> <span class="err">│</span> <span class="mi">10</span> <span class="err">│</span> <span class="mi">5</span> <span class="err">│</span> <span class="mi">27</span> <span class="err">│</span> <span class="mi">2022</span><span class="o">-</span><span class="mi">12</span><span class="o">-</span><span class="mi">05</span> <span class="mi">21</span><span class="p">:</span><span class="mi">46</span><span class="p">:</span><span class="mi">01</span><span class="p">.</span><span class="mi">583185</span>
<span class="mi">13</span> <span class="err">│</span> <span class="mi">6</span> <span class="err">│</span> <span class="mi">7</span> <span class="err">│</span> <span class="mi">5</span> <span class="err">│</span> <span class="mi">26</span> <span class="err">│</span> <span class="mi">2022</span><span class="o">-</span><span class="mi">12</span><span class="o">-</span><span class="mi">23</span> <span class="mi">14</span><span class="p">:</span><span class="mi">36</span><span class="p">:</span><span class="mi">48</span><span class="p">.</span><span class="mi">366411</span>
<span class="mi">19</span> <span class="err">│</span> <span class="mi">10</span> <span class="err">│</span> <span class="mi">9</span> <span class="err">│</span> <span class="mi">5</span> <span class="err">│</span> <span class="mi">39</span> <span class="err">│</span> <span class="mi">2023</span><span class="o">-</span><span class="mi">03</span><span class="o">-</span><span class="mi">01</span> <span class="mi">23</span><span class="p">:</span><span class="mi">28</span><span class="p">:</span><span class="mi">32</span><span class="p">.</span><span class="mi">880234</span>
<span class="mi">20</span> <span class="err">│</span> <span class="mi">9</span> <span class="err">│</span> <span class="mi">10</span> <span class="err">│</span> <span class="mi">5</span> <span class="err">│</span> <span class="mi">86</span> <span class="err">│</span> <span class="mi">2023</span><span class="o">-</span><span class="mi">03</span><span class="o">-</span><span class="mi">01</span> <span class="mi">23</span><span class="p">:</span><span class="mi">35</span><span class="p">:</span><span class="mi">15</span><span class="p">.</span><span class="mi">564763</span>
<span class="p">(</span><span class="mi">4</span> <span class="k">rows</span><span class="p">)</span>
</code></pre></div></div>
<p>Note the project_id is distinct compared to the all records. And the timestamps are the most recent for those duplicated projects (their id = 26, 39).</p>
<p>There is no way to solve this task efficiently using only ActiveRecord functionality and pure Ruby. But SQL can solve this with the <a href="https://www.postgresql.org/docs/current/tutorial-window.html" ref="nofollow" target="_blank">window function</a> technique.</p>
<h2 id="how-window-function-with-row-number-partition-works">How window function with row number partition works</h2>
<p>The idea is the following - we rank all records inside the table from 1 no N for the duplicated records of our search criteria. The most recent record gets 1, older one gets higher rank. The distinct rows will have 1.</p>
<p>For example:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="n">id</span> <span class="err">│</span> <span class="n">project_id</span> <span class="err">│</span> <span class="n">reviewee_id</span> <span class="err">│</span> <span class="n">created_at</span> <span class="err">│</span> <span class="n">row_number</span>
<span class="err">════╪════════════╪═════════════╪════════════════════════════╪════════════</span>
<span class="mi">8</span> <span class="err">│</span> <span class="mi">27</span> <span class="err">│</span> <span class="mi">10</span> <span class="err">│</span> <span class="mi">2022</span><span class="o">-</span><span class="mi">12</span><span class="o">-</span><span class="mi">05</span> <span class="mi">21</span><span class="p">:</span><span class="mi">46</span><span class="p">:</span><span class="mi">01</span><span class="p">.</span><span class="mi">583185</span> <span class="err">│</span> <span class="mi">1</span>
<span class="mi">12</span> <span class="err">│</span> <span class="mi">26</span> <span class="err">│</span> <span class="mi">6</span> <span class="err">│</span> <span class="mi">2022</span><span class="o">-</span><span class="mi">12</span><span class="o">-</span><span class="mi">23</span> <span class="mi">14</span><span class="p">:</span><span class="mi">35</span><span class="p">:</span><span class="mi">11</span><span class="p">.</span><span class="mi">047002</span> <span class="err">│</span> <span class="mi">2</span>
<span class="mi">13</span> <span class="err">│</span> <span class="mi">26</span> <span class="err">│</span> <span class="mi">7</span> <span class="err">│</span> <span class="mi">2022</span><span class="o">-</span><span class="mi">12</span><span class="o">-</span><span class="mi">23</span> <span class="mi">14</span><span class="p">:</span><span class="mi">36</span><span class="p">:</span><span class="mi">48</span><span class="p">.</span><span class="mi">366411</span> <span class="err">│</span> <span class="mi">1</span>
<span class="mi">18</span> <span class="err">│</span> <span class="mi">39</span> <span class="err">│</span> <span class="mi">10</span> <span class="err">│</span> <span class="mi">2023</span><span class="o">-</span><span class="mi">03</span><span class="o">-</span><span class="mi">01</span> <span class="mi">23</span><span class="p">:</span><span class="mi">27</span><span class="p">:</span><span class="mi">52</span><span class="p">.</span><span class="mi">68548</span> <span class="err">│</span> <span class="mi">2</span>
<span class="mi">19</span> <span class="err">│</span> <span class="mi">39</span> <span class="err">│</span> <span class="mi">9</span> <span class="err">│</span> <span class="mi">2023</span><span class="o">-</span><span class="mi">03</span><span class="o">-</span><span class="mi">01</span> <span class="mi">23</span><span class="p">:</span><span class="mi">28</span><span class="p">:</span><span class="mi">32</span><span class="p">.</span><span class="mi">880234</span> <span class="err">│</span> <span class="mi">1</span>
<span class="mi">20</span> <span class="err">│</span> <span class="mi">86</span> <span class="err">│</span> <span class="mi">10</span> <span class="err">│</span> <span class="mi">2023</span><span class="o">-</span><span class="mi">03</span><span class="o">-</span><span class="mi">01</span> <span class="mi">23</span><span class="p">:</span><span class="mi">35</span><span class="p">:</span><span class="mi">15</span><span class="p">.</span><span class="mi">564763</span> <span class="err">│</span> <span class="mi">1</span>
<span class="p">(</span><span class="mi">6</span> <span class="k">rows</span><span class="p">)</span>
</code></pre></div></div>
<p>The ratings with id = 8, 20 receive row number 1 because these projects are distinct (27, 86). But projects with id = 26, 39 have several ratings that’s why the rows with this project id have row_number 1 and 2. The most recent ratings per project receive 1, and the older ones receive row number 2.</p>
<h2 id="use-subselect-to-filter-correct-results">Use subselect to filter correct results</h2>
<p>If we filter out those row numbers greater 1 we get the required result. If that would be a table we could use the SQL’s <code class="highlighter-rouge">where</code> clause. For example, a view (virtual table) could be created for that. But we will keep it simple. We will use subselect: initially we prepare select to return the data as above and immediately use <code class="highlighter-rouge">select</code> statement to filter out the correct result.</p>
<p>But first, let’s see how to write SQL statement to assign the row number using the already noticed <strong>window function</strong>:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">db</span><span class="o">-#</span> <span class="k">select</span>
<span class="n">id</span><span class="p">,</span>
<span class="n">project_id</span><span class="p">,</span>
<span class="n">reviewee_id</span><span class="p">,</span>
<span class="n">created_at</span><span class="p">,</span>
<span class="n">row_number</span><span class="p">()</span> <span class="n">over</span> <span class="p">(</span><span class="n">partition</span> <span class="k">by</span> <span class="n">project_id</span> <span class="k">order</span> <span class="k">by</span> <span class="n">created_at</span> <span class="k">desc</span><span class="p">)</span>
<span class="k">from</span> <span class="n">ratings</span>
<span class="k">order</span> <span class="k">by</span> <span class="n">created_at</span><span class="p">;</span>
<span class="n">id</span> <span class="err">│</span> <span class="n">project_id</span> <span class="err">│</span> <span class="n">reviewee_id</span> <span class="err">│</span> <span class="n">created_at</span> <span class="err">│</span> <span class="n">row_number</span>
<span class="err">════╪════════════╪═════════════╪════════════════════════════╪════════════</span>
<span class="mi">8</span> <span class="err">│</span> <span class="mi">27</span> <span class="err">│</span> <span class="mi">10</span> <span class="err">│</span> <span class="mi">2022</span><span class="o">-</span><span class="mi">12</span><span class="o">-</span><span class="mi">05</span> <span class="mi">21</span><span class="p">:</span><span class="mi">46</span><span class="p">:</span><span class="mi">01</span><span class="p">.</span><span class="mi">583185</span> <span class="err">│</span> <span class="mi">1</span>
<span class="mi">12</span> <span class="err">│</span> <span class="mi">26</span> <span class="err">│</span> <span class="mi">6</span> <span class="err">│</span> <span class="mi">2022</span><span class="o">-</span><span class="mi">12</span><span class="o">-</span><span class="mi">23</span> <span class="mi">14</span><span class="p">:</span><span class="mi">35</span><span class="p">:</span><span class="mi">11</span><span class="p">.</span><span class="mi">047002</span> <span class="err">│</span> <span class="mi">2</span>
<span class="mi">13</span> <span class="err">│</span> <span class="mi">26</span> <span class="err">│</span> <span class="mi">7</span> <span class="err">│</span> <span class="mi">2022</span><span class="o">-</span><span class="mi">12</span><span class="o">-</span><span class="mi">23</span> <span class="mi">14</span><span class="p">:</span><span class="mi">36</span><span class="p">:</span><span class="mi">48</span><span class="p">.</span><span class="mi">366411</span> <span class="err">│</span> <span class="mi">1</span>
<span class="mi">18</span> <span class="err">│</span> <span class="mi">39</span> <span class="err">│</span> <span class="mi">10</span> <span class="err">│</span> <span class="mi">2023</span><span class="o">-</span><span class="mi">03</span><span class="o">-</span><span class="mi">01</span> <span class="mi">23</span><span class="p">:</span><span class="mi">27</span><span class="p">:</span><span class="mi">52</span><span class="p">.</span><span class="mi">68548</span> <span class="err">│</span> <span class="mi">2</span>
<span class="mi">19</span> <span class="err">│</span> <span class="mi">39</span> <span class="err">│</span> <span class="mi">9</span> <span class="err">│</span> <span class="mi">2023</span><span class="o">-</span><span class="mi">03</span><span class="o">-</span><span class="mi">01</span> <span class="mi">23</span><span class="p">:</span><span class="mi">28</span><span class="p">:</span><span class="mi">32</span><span class="p">.</span><span class="mi">880234</span> <span class="err">│</span> <span class="mi">1</span>
<span class="mi">20</span> <span class="err">│</span> <span class="mi">86</span> <span class="err">│</span> <span class="mi">10</span> <span class="err">│</span> <span class="mi">2023</span><span class="o">-</span><span class="mi">03</span><span class="o">-</span><span class="mi">01</span> <span class="mi">23</span><span class="p">:</span><span class="mi">35</span><span class="p">:</span><span class="mi">15</span><span class="p">.</span><span class="mi">564763</span> <span class="err">│</span> <span class="mi">1</span>
<span class="p">(</span><span class="mi">6</span> <span class="k">rows</span><span class="p">)</span>
</code></pre></div></div>
<p>The <code class="highlighter-rouge">row_number() over (partition by project_id order by created_at desc)</code> is a window function that assigns row number from 1 to N for the records duplicated by some criteria. In this case the criteria is distinct project_id sorted by created_at desc.</p>
<p>Running this query inside DB console will produce the result above.</p>
<p>Wrap this <code class="highlighter-rouge">select</code> with another <code class="highlighter-rouge">select</code> and filter only rows with number = 1:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">db</span><span class="o">-#</span> <span class="k">select</span>
<span class="n">id</span><span class="p">,</span>
<span class="n">reviewer_id</span><span class="p">,</span>
<span class="n">reviewee_id</span><span class="p">,</span>
<span class="n">rating</span><span class="p">,</span>
<span class="n">project_id</span><span class="p">,</span>
<span class="n">created_at</span> <span class="k">from</span> <span class="p">(</span>
<span class="k">select</span> <span class="o">*</span><span class="p">,</span> <span class="n">row_number</span><span class="p">()</span> <span class="n">over</span> <span class="p">(</span><span class="n">partition</span> <span class="k">by</span> <span class="n">project_id</span> <span class="k">order</span> <span class="k">by</span> <span class="n">created_at</span> <span class="k">desc</span><span class="p">)</span>
<span class="k">from</span> <span class="n">ratings</span>
<span class="k">order</span> <span class="k">by</span> <span class="n">created_at</span>
<span class="p">)</span>
<span class="k">as</span> <span class="n">ratings</span> <span class="k">where</span> <span class="n">row_number</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="n">id</span> <span class="err">│</span> <span class="n">reviewer_id</span> <span class="err">│</span> <span class="n">reviewee_id</span> <span class="err">│</span> <span class="n">rating</span> <span class="err">│</span> <span class="n">project_id</span> <span class="err">│</span> <span class="n">created_at</span>
<span class="err">════╪═════════════╪═════════════╪════════╪════════════╪════════════════════════════</span>
<span class="mi">8</span> <span class="err">│</span> <span class="mi">9</span> <span class="err">│</span> <span class="mi">10</span> <span class="err">│</span> <span class="mi">5</span> <span class="err">│</span> <span class="mi">27</span> <span class="err">│</span> <span class="mi">2022</span><span class="o">-</span><span class="mi">12</span><span class="o">-</span><span class="mi">05</span> <span class="mi">21</span><span class="p">:</span><span class="mi">46</span><span class="p">:</span><span class="mi">01</span><span class="p">.</span><span class="mi">583185</span>
<span class="mi">13</span> <span class="err">│</span> <span class="mi">6</span> <span class="err">│</span> <span class="mi">7</span> <span class="err">│</span> <span class="mi">5</span> <span class="err">│</span> <span class="mi">26</span> <span class="err">│</span> <span class="mi">2022</span><span class="o">-</span><span class="mi">12</span><span class="o">-</span><span class="mi">23</span> <span class="mi">14</span><span class="p">:</span><span class="mi">36</span><span class="p">:</span><span class="mi">48</span><span class="p">.</span><span class="mi">366411</span>
<span class="mi">19</span> <span class="err">│</span> <span class="mi">10</span> <span class="err">│</span> <span class="mi">9</span> <span class="err">│</span> <span class="mi">5</span> <span class="err">│</span> <span class="mi">39</span> <span class="err">│</span> <span class="mi">2023</span><span class="o">-</span><span class="mi">03</span><span class="o">-</span><span class="mi">01</span> <span class="mi">23</span><span class="p">:</span><span class="mi">28</span><span class="p">:</span><span class="mi">32</span><span class="p">.</span><span class="mi">880234</span>
<span class="mi">20</span> <span class="err">│</span> <span class="mi">9</span> <span class="err">│</span> <span class="mi">10</span> <span class="err">│</span> <span class="mi">5</span> <span class="err">│</span> <span class="mi">86</span> <span class="err">│</span> <span class="mi">2023</span><span class="o">-</span><span class="mi">03</span><span class="o">-</span><span class="mi">01</span> <span class="mi">23</span><span class="p">:</span><span class="mi">35</span><span class="p">:</span><span class="mi">15</span><span class="p">.</span><span class="mi">564763</span>
<span class="p">(</span><span class="mi">4</span> <span class="k">rows</span><span class="p">)</span>
</code></pre></div></div>
<p>Voila, we’ve got what we want!</p>
<h2 id="use-activerecordfrom-to-return-the-results-as-ruby-objects">Use ActiveRecord.from to return the results as Ruby objects</h2>
<p>Since we’ve got the SQL query it’s easy to port it into ActiveRecord and get eventually the list of Ruby objects. We will use the <code class="highlighter-rouge">ActiveRecord.from</code> to write the subselect:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">></span> <span class="no">Rating</span>
<span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="s2">"id, reviewer_id, reviewee_id, rating, project_id, created_at"</span><span class="p">)</span>
<span class="p">.</span><span class="nf">from</span><span class="p">(</span><span class="s2">"(select *, row_number() over (partition by project_id order by created_at desc) from ratings group by project_id, reviewee_id, created_at, id order by created_at) as ratings"</span><span class="p">)</span>
<span class="no">Rating</span> <span class="no">Load</span> <span class="p">(</span><span class="mf">41.9</span><span class="n">ms</span><span class="p">)</span> <span class="no">SELECT</span> <span class="nb">id</span><span class="p">,</span> <span class="n">reviewer_id</span><span class="p">,</span> <span class="n">reviewee_id</span><span class="p">,</span> <span class="n">rating</span><span class="p">,</span> <span class="n">project_id</span><span class="p">,</span> <span class="n">created_at</span> <span class="no">FROM</span> <span class="p">(</span><span class="nb">select</span> <span class="o">*</span><span class="p">,</span> <span class="n">row_number</span><span class="p">()</span> <span class="n">over</span> <span class="p">(</span><span class="n">partition</span> <span class="n">by</span> <span class="n">project_id</span> <span class="n">order</span> <span class="n">by</span> <span class="n">created_at</span> <span class="n">desc</span><span class="p">)</span> <span class="n">from</span> <span class="n">ratings</span> <span class="n">group</span> <span class="n">by</span> <span class="n">project_id</span><span class="p">,</span> <span class="n">reviewee_id</span><span class="p">,</span> <span class="n">created_at</span><span class="p">,</span> <span class="nb">id</span> <span class="n">order</span> <span class="n">by</span> <span class="n">created_at</span><span class="p">)</span> <span class="n">as</span> <span class="n">ratings</span> <span class="no">WHERE</span> <span class="s2">"ratings"</span><span class="o">.</span><span class="s2">"row_number"</span> <span class="o">=</span> <span class="vg">$1</span> <span class="p">[[</span><span class="s2">"row_number"</span><span class="p">,</span> <span class="mi">1</span><span class="p">]]</span>
<span class="o">=></span> <span class="p">[</span><span class="c1">#<Rating:0x000000011190d410 id: 8, reviewer_id: 9, reviewee_id: 10, rating: 5, project_id: 27, created_at: Mon, 05 Dec 2022 21:46:01.583185000 UTC +00:00>,</span>
<span class="c1">#<Rating:0x000000011190d348 id: 13, reviewer_id: 6, reviewee_id: 7, rating: 5, project_id: 26, created_at: Fri, 23 Dec 2022 14:36:48.366411000 UTC +00:00>,</span>
<span class="c1">#<Rating:0x000000011190d280 id: 19, reviewer_id: 10, reviewee_id: 9, rating: 5, project_id: 39, created_at: Wed, 01 Mar 2023 23:28:32.880234000 UTC +00:00>,</span>
<span class="c1">#<Rating:0x000000011190d1b8 id: 20, reviewer_id: 9, reviewee_id: 10, rating: 5, project_id: 86, created_at: Wed, 01 Mar 2023 23:35:15.564763000 UTC +00:00>]</span>
</code></pre></div></div>
<p>You can run this experiment yourself on this <a href="https://github.com/widefix/demo-fast-sql" ref="nofollow" target="_blank">demo app</a>.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Advanced SQL understanding allows you to write performant advanced functionality in a Ruby on Rails application efficiently.</p>
<p>If you like this article and would like to see more examples of how SQL can improve your software development life read these articles:</p>
<ul>
<li><a href="https:http://blog.widefix.com/importance-sql-for-rails-experts/" ref="nofollow" target="_blank">Make your Ruby on Rails app 80x faster with SQL</a></li>
<li><a href="https:http://blog.widefix.com/financial-plan-on-postgresql/" ref="nofollow" target="_blank">Financial plan on PostgreSQL</a></li>
<li><a href="https:http://blog.widefix.com/financial-plan-on-rails/" ref="nofollow" target="_blank">Financial plan on Rails</a></li>
<li><a href="https:http://blog.widefix.com/from-single-dd-to-multiple-checkboxes/" ref="nofollow" target="_blank">From Single drop-down to Multiple check-boxes</a></li>
<li><a href="https:http://blog.widefix.com/date-ranges-overlap/" ref="nofollow" target="_blank">Efficient algorithm to check dates overlap</a></li>
</ul>
<p>Have a good day ahead and happy coding!</p>
<p><a href="http://blog.widefix.com/select-unique-latest-grouped-records-from-db/">Select unique latest grouped records from DB</a> was originally published by Andrei Kaleshka at <a href="http://blog.widefix.com">Take your Rails development to the next level</a> on July 13, 2023.</p>http://blog.widefix.com/use-timestamp-attributes-as-predicates-in-rails2023-06-15 15:19:16 +0200T00:00:00-00:002023-06-15T00:00:00-04:00Andrei Kaleshkahttp://blog.widefix.comka8725@gmail.com
<h2 id="why-use-timestamp-field-instead-of-boolean-for-checkbox">Why use timestamp field instead of boolean for checkbox</h2>
<p>You are about to add a new boolean attribute into your model inside a Ruby on Rails application. It’s simple as is - add a boolean column in DB and call it a day. But stop and think - consider a timestamp type for the corresponding columns in the database.</p>
<p>This approach allows for tracking the time when the value change. That can be helpful in debugging, especially when dealing with production issues.</p>
<p>Ok, it’s preferable to use the timestamp field type on the back-end. But it is still convenient to have a checkbox on the user interface. Thus, the timestamp column type does not match the UI field type (checkbox). That means the value (such as true/false, yes/no, or any other) from the front-end should be transformed into a timestamp or <code class="highlighter-rouge">nil</code> on the back-end. That’s needed, to ensure compatibility with the mass-assignment methods (<code class="highlighter-rouge">.new</code> or <code class="highlighter-rouge">#update</code>).</p>
<h2 id="avoid-code-duplicating-with-a-universal-solution">Avoid code duplicating with a universal solution</h2>
<p>One could write this type of transformation logic ad-hoc inside controllers. But you can create a universal solution. One way to achieve this is by defining a helper method in the base model class. In Rails, the base model class is typically <code class="highlighter-rouge">ApplicationRecord</code>. By doing this, you can ensure its availability across all models in the application. See the following example how to do this:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ApplicationRecord</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="n">primary_abstract_class</span>
<span class="c1"># Defines a predicate method and a setter method for the specified attributes.</span>
<span class="c1">#</span>
<span class="c1"># To use this code in the User model, call the following:</span>
<span class="c1"># timestamp_as_boolean :muted</span>
<span class="c1">#</span>
<span class="c1"># In this scenario, it assumes that a "muted_at" column is defined in the table.</span>
<span class="c1"># After calling the code, the following methods are defined on the User model:</span>
<span class="c1"># muted? - checks if a user is muted</span>
<span class="c1"># muted=(value) - sets the muted_at timestamp if the value is equivalent to boolean "true"</span>
<span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">timestamp_as_boolean</span><span class="p">(</span><span class="o">*</span><span class="n">fields</span><span class="p">)</span>
<span class="n">fields</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">field</span><span class="o">|</span>
<span class="n">timestamp_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">_at"</span>
<span class="n">predicate</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">?"</span>
<span class="n">define_method</span><span class="p">(</span><span class="n">predicate</span><span class="p">)</span> <span class="k">do</span>
<span class="n">public_send</span><span class="p">(</span><span class="n">timestamp_field</span><span class="p">).</span><span class="nf">present?</span>
<span class="k">end</span>
<span class="n">define_method</span><span class="p">(</span><span class="s2">"</span><span class="si">#{</span><span class="n">field</span><span class="si">}</span><span class="s2">="</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">value</span><span class="o">|</span>
<span class="n">new_value</span> <span class="o">=</span> <span class="no">ActiveModel</span><span class="o">::</span><span class="no">Type</span><span class="o">::</span><span class="no">Boolean</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">cast</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="p">?</span> <span class="no">Time</span><span class="p">.</span><span class="nf">current</span> <span class="p">:</span> <span class="kp">nil</span>
<span class="n">public_send</span><span class="p">(</span><span class="s2">"</span><span class="si">#{</span><span class="n">timestamp_field</span><span class="si">}</span><span class="s2">="</span><span class="p">,</span> <span class="n">new_value</span><span class="p">)</span> <span class="k">if</span> <span class="n">new_value</span><span class="p">.</span><span class="nf">nil?</span> <span class="o">||</span> <span class="o">!</span><span class="n">public_send</span><span class="p">(</span><span class="n">predicate</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></div></div>
<p>We have now implemented a universal solution using some meta-programming techniques in Ruby. Here’s how you can use it. Let’s take the example of the <code class="highlighter-rouge">User</code> model and convert the <code class="highlighter-rouge">muted_at</code> column into a boolean attribute using the following approach:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">timestamp_as_boolean</span> <span class="ss">:muted</span>
<span class="k">end</span>
</code></pre></div></div>
<p>To utilize this solution in an ERB template on the user interface, follow these steps:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o"><</span><span class="sx">%= form.check_box :muted, { checked: @user.muted? } %>
</span></code></pre></div></div>
<p>Let’s verify its functionality:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>> user = User.last
> user.muted? # => true
> user.update!(muted: false)
> user.muted? # => false
> user.update!(muted: true) # => true
> user.muted? # => true
> user.muted_at # => Thu, 15 Jun 2023 15:16:54.582848000 UTC +00:00
> user.update!(muted: true) # => true
> user.muted_at # => Thu, 15 Jun 2023 15:16:54.582848000 UTC +00:00
</code></pre></div></div>
<p>It is important to note that after the second assignment of muted = true, the timestamp did not change. This behavior is expected and logical because the value itself was not modified. The purpose of tracking the timestamp is to capture the moment when a change in the value occurs. In this case, since the value remains the same, there is no need for the timestamp to be updated. This behavior aligns with the intended functionality. It ensures that the timestamp accurately reflects when a change in the value of the attribute occurs.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Use a timestamp type for columns in the database. Create a universal solution to transform boolean values. As a result, enhance the functionality and debugging capabilities of our models. The timestamp type allows us to track the values change time. That can aid in identifying and troubleshooting issues, particularly in production environments. Avoid code duplication and ensure consistent behavior across models with a universal solution. Metaprogramming can help with that. These improvements contribute to a more robust and efficient application.</p>
<p>Happy coding!</p>
<p><a href="http://blog.widefix.com/use-timestamp-attributes-as-predicates-in-rails/">Use timestamp for predicates in Rails</a> was originally published by Andrei Kaleshka at <a href="http://blog.widefix.com">Take your Rails development to the next level</a> on June 15, 2023.</p>http://blog.widefix.com/optimize-performance-of-rails-app-with-chatgpt2023-06-07 18:30:34 +0200T00:00:00-00:002023-06-07T00:00:00-04:00Andrei Kaleshkahttp://blog.widefix.comka8725@gmail.com
<p>The previous article <a href="https:http://blog.widefix.com/importance-sql-for-rails-experts/" ref="nofollow" target="_blank">Make your Ruby on Rails app 80x faster with SQL</a> shows how SQL knowledge can help to optimize your Rails application performance. We discussed it within <a href="https://www.brug.by/" ref="nofollow" target="_blank">Belarus User Group</a> community. Not everyone agreed with the point and didn’t find SQL knowledge a good asset for investment. That’s something expected. There is no revelation here. But that meeting had something that everyone was impressed with. We experimented and found out how ChatGPT is good with code optimization. We saw how it transforms Ruby code into performant SQL. The results were great. Check that out within this article.</p>
<h2 id="chatgpt-request-to-transform-ruby-code-into-sql">ChatGPT request to transform Ruby code into SQL</h2>
<p>As before, we are going to use this <a href="https://github.com/widefix/demo-fast-sql" ref="nofollow" target="_blank">experimental repository</a>.</p>
<p>We took <a href="https://github.com/widefix/demo-fast-sql/blob/4ea63b68d4404403e543b1d09978e5cdd5742f36/app/queries/services_stats_query.rb#L31-L64" ref="nofollow" target="_blank">the following code snippet</a>, the slowest and original version, and asked to improve its performance by rewriting it into SQL:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># the original code</span>
<span class="n">projects_full</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">projects_empty</span> <span class="o">=</span> <span class="p">[]</span>
<span class="no">Service</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">user: </span><span class="n">user</span><span class="p">,</span> <span class="ss">status: </span><span class="s2">"approved"</span><span class="p">,</span> <span class="ss">active: </span><span class="kp">true</span><span class="p">).</span><span class="nf">order</span><span class="p">(</span><span class="ss">category_id: :asc</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">service</span><span class="o">|</span>
<span class="n">ratings_average</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">ratings_count</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">ratings_total</span> <span class="o">=</span> <span class="mi">0</span>
<span class="no">Rating</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">reviewee: </span><span class="n">user</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">rating</span><span class="o">|</span>
<span class="n">project</span> <span class="o">=</span> <span class="no">Project</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">rating</span><span class="p">.</span><span class="nf">project_id</span><span class="p">)</span>
<span class="k">if</span> <span class="n">project</span><span class="p">.</span><span class="nf">category_id</span> <span class="o">==</span> <span class="n">service</span><span class="p">.</span><span class="nf">category_id</span>
<span class="n">ratings_count</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="n">ratings_total</span> <span class="o">+=</span> <span class="n">rating</span><span class="p">.</span><span class="nf">rating</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">ratings_average</span> <span class="o">=</span> <span class="p">(</span><span class="n">ratings_total</span> <span class="o">/</span> <span class="n">ratings_count</span><span class="p">.</span><span class="nf">to_f</span><span class="p">).</span><span class="nf">round</span><span class="p">(</span><span class="mi">1</span><span class="p">).</span><span class="nf">to_s</span> <span class="k">if</span> <span class="n">ratings_count</span> <span class="o">!=</span> <span class="mi">0</span> <span class="o">&&</span> <span class="n">ratings_total</span> <span class="o">!=</span> <span class="mi">0</span>
<span class="n">completed_projects_count</span> <span class="o">=</span> <span class="no">Project</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">vendor: </span><span class="n">user</span><span class="p">,</span> <span class="ss">status: </span><span class="s2">"Complete"</span><span class="p">,</span> <span class="ss">category_id: </span><span class="n">service</span><span class="p">.</span><span class="nf">category_id</span><span class="p">).</span><span class="nf">size</span>
<span class="n">service_hash</span> <span class="o">=</span> <span class="p">{</span>
<span class="ss">category_id: </span><span class="n">service</span><span class="p">.</span><span class="nf">category_id</span><span class="p">,</span>
<span class="ss">category_name: </span><span class="no">Category</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="no">Category</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">service</span><span class="p">.</span><span class="nf">category_id</span><span class="p">).</span><span class="nf">parent_id</span><span class="p">).</span><span class="nf">name</span><span class="p">,</span>
<span class="ss">subcategory_name: </span><span class="no">Category</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">service</span><span class="p">.</span><span class="nf">category_id</span><span class="p">).</span><span class="nf">name</span><span class="p">,</span>
<span class="ss">completed_projects_count: </span><span class="n">completed_projects_count</span><span class="p">,</span>
<span class="ss">ratings_count: </span><span class="n">ratings_count</span><span class="p">,</span>
<span class="ss">ratings_average: </span><span class="n">ratings_average</span><span class="p">,</span>
<span class="p">}</span>
<span class="n">resolved_hash</span> <span class="o">=</span> <span class="n">service_hash</span><span class="p">.</span><span class="nf">transform_keys</span><span class="p">{</span> <span class="o">|</span><span class="n">k</span><span class="o">|</span> <span class="n">k</span><span class="p">.</span><span class="nf">to_s</span><span class="p">.</span><span class="nf">camelize</span><span class="p">(</span><span class="ss">:lower</span><span class="p">)</span> <span class="p">}</span>
<span class="k">if</span> <span class="n">completed_projects_count</span> <span class="o">></span> <span class="mi">0</span>
<span class="n">projects_full</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="n">resolved_hash</span><span class="p">)</span>
<span class="k">else</span>
<span class="n">projects_empty</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="n">resolved_hash</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">projects_full</span><span class="p">.</span><span class="nf">sort_by!</span><span class="p">{</span> <span class="o">|</span><span class="n">k</span><span class="o">|</span> <span class="o">-</span><span class="n">k</span><span class="p">[</span><span class="s2">"completedProjectsCount"</span><span class="p">]</span> <span class="p">}</span>
<span class="n">projects_full</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="o">*</span><span class="n">projects_empty</span><span class="p">)</span>
<span class="n">projects_full</span>
</code></pre></div></div>
<p>And we got the following response from ChatGPT:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">SELECT</span>
<span class="n">c1</span><span class="p">.</span><span class="nf">id</span> <span class="no">AS</span> <span class="n">category_id</span><span class="p">,</span>
<span class="n">c2</span><span class="p">.</span><span class="nf">name</span> <span class="no">AS</span> <span class="n">category_name</span><span class="p">,</span>
<span class="n">c1</span><span class="p">.</span><span class="nf">name</span> <span class="no">AS</span> <span class="n">subcategory_name</span><span class="p">,</span>
<span class="no">COUNT</span><span class="p">(</span><span class="no">DISTINCT</span> <span class="nb">p</span><span class="p">.</span><span class="nf">id</span><span class="p">)</span> <span class="no">FILTER</span> <span class="p">(</span><span class="no">WHERE</span> <span class="nb">p</span><span class="p">.</span><span class="nf">status</span> <span class="o">=</span> <span class="s1">'Complete'</span><span class="p">)</span> <span class="no">AS</span> <span class="n">completed_projects_count</span><span class="p">,</span>
<span class="no">COUNT</span><span class="p">(</span><span class="n">r</span><span class="p">.</span><span class="nf">id</span><span class="p">)</span> <span class="no">AS</span> <span class="n">ratings_count</span><span class="p">,</span>
<span class="no">COALESCE</span><span class="p">(</span><span class="no">AVG</span><span class="p">(</span><span class="n">r</span><span class="p">.</span><span class="nf">rating</span><span class="p">),</span> <span class="mi">0</span><span class="p">)</span> <span class="no">AS</span> <span class="n">ratings_average</span>
<span class="no">FROM</span>
<span class="n">services</span> <span class="n">s</span>
<span class="no">JOIN</span> <span class="n">categories</span> <span class="n">c1</span> <span class="no">ON</span> <span class="n">s</span><span class="p">.</span><span class="nf">category_id</span> <span class="o">=</span> <span class="n">c1</span><span class="p">.</span><span class="nf">id</span>
<span class="no">JOIN</span> <span class="n">categories</span> <span class="n">c2</span> <span class="no">ON</span> <span class="n">c1</span><span class="p">.</span><span class="nf">parent_id</span> <span class="o">=</span> <span class="n">c2</span><span class="p">.</span><span class="nf">id</span>
<span class="no">LEFT</span> <span class="no">JOIN</span> <span class="n">projects</span> <span class="nb">p</span> <span class="no">ON</span> <span class="n">s</span><span class="p">.</span><span class="nf">user_id</span> <span class="o">=</span> <span class="nb">p</span><span class="p">.</span><span class="nf">vendor_id</span> <span class="no">AND</span> <span class="nb">p</span><span class="p">.</span><span class="nf">category_id</span> <span class="o">=</span> <span class="n">c1</span><span class="p">.</span><span class="nf">id</span>
<span class="no">LEFT</span> <span class="no">JOIN</span> <span class="n">ratings</span> <span class="n">r</span> <span class="no">ON</span> <span class="n">s</span><span class="p">.</span><span class="nf">user_id</span> <span class="o">=</span> <span class="n">r</span><span class="p">.</span><span class="nf">reviewee_id</span> <span class="no">AND</span> <span class="nb">p</span><span class="p">.</span><span class="nf">id</span> <span class="o">=</span> <span class="n">r</span><span class="p">.</span><span class="nf">project_id</span>
<span class="no">WHERE</span>
<span class="n">s</span><span class="p">.</span><span class="nf">user_id</span> <span class="o">=</span> <span class="ss">:user_id</span> <span class="no">AND</span> <span class="n">s</span><span class="p">.</span><span class="nf">status</span> <span class="o">=</span> <span class="s1">'approved'</span> <span class="no">AND</span> <span class="n">s</span><span class="p">.</span><span class="nf">active</span> <span class="o">=</span> <span class="kp">true</span>
<span class="no">GROUP</span> <span class="no">BY</span>
<span class="n">c1</span><span class="p">.</span><span class="nf">id</span><span class="p">,</span> <span class="n">c2</span><span class="p">.</span><span class="nf">name</span><span class="p">,</span> <span class="n">c1</span><span class="p">.</span><span class="nf">name</span>
<span class="no">ORDER</span> <span class="no">BY</span>
<span class="n">completed_projects_count</span> <span class="no">DESC</span><span class="p">,</span> <span class="n">c1</span><span class="p">.</span><span class="nf">id</span> <span class="no">ASC</span><span class="p">;</span>
</code></pre></div></div>
<p>And these are the performance measurement results:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Warming up --------------------------------------
ruby 1.000 i/100ms
sql 54.000 i/100ms
sql_gpt 59.000 i/100ms
Calculating -------------------------------------
ruby 6.846 (±14.6%) i/s - 34.000 in 5.000338s
sql 540.320 (± 8.5%) i/s - 2.700k in 5.038711s
sql_gpt 609.335 (± 6.9%) i/s - 3.068k in 5.059364s
Comparison:
sql_gpt: 609.3 i/s
sql: 540.3 i/s - same-ish: difference falls within error
ruby: 6.8 i/s - 89.01x slower
</code></pre></div></div>
<p>We also checked that the result of the query service is the same for both calls. That means there are no bugs in the produced SQL by the AI.</p>
<p>The ChatGPT result is the fastest one. At the same time, no business logic was defective. That’s a very good result!</p>
<h2 id="should-one-use-chatgpt-to-optimize-code-performance-or-not">Should one use ChatGPT to optimize code performance or not</h2>
<p>While the AI results are good, it’s still doubtful who and how to use the tool. Not seasoned developers might don’t understand where the bottleneck is. They would struggle with finding a correct question for ChatGPT. They would not understand how to fix the generated code if it has bugs. Some time later I tried to repeat the experiment and know the machine produced a very different SQL that has bugs inside:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Get average ratings and count for each service</span>
<span class="k">WITH</span> <span class="n">rating_summary</span> <span class="k">AS</span> <span class="p">(</span>
<span class="k">SELECT</span> <span class="n">p</span><span class="p">.</span><span class="n">category_id</span><span class="p">,</span> <span class="k">COUNT</span><span class="p">(</span><span class="n">r</span><span class="p">.</span><span class="n">id</span><span class="p">)</span> <span class="k">AS</span> <span class="n">ratings_count</span><span class="p">,</span> <span class="n">COALESCE</span><span class="p">(</span><span class="k">AVG</span><span class="p">(</span><span class="n">r</span><span class="p">.</span><span class="n">rating</span><span class="p">),</span> <span class="mi">0</span><span class="p">)</span> <span class="k">AS</span> <span class="n">ratings_average</span>
<span class="k">FROM</span> <span class="n">ratings</span> <span class="n">r</span>
<span class="k">INNER</span> <span class="k">JOIN</span> <span class="n">projects</span> <span class="n">p</span> <span class="k">ON</span> <span class="n">p</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="n">r</span><span class="p">.</span><span class="n">project_id</span>
<span class="k">WHERE</span> <span class="n">r</span><span class="p">.</span><span class="n">reviewee</span> <span class="o">=</span> <span class="o"><</span><span class="n">user_id</span><span class="o">></span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="n">p</span><span class="p">.</span><span class="n">category_id</span>
<span class="p">),</span>
<span class="c1">-- Get completed projects count for each service</span>
<span class="n">completed_projects</span> <span class="k">AS</span> <span class="p">(</span>
<span class="k">SELECT</span> <span class="n">category_id</span><span class="p">,</span> <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">AS</span> <span class="n">completed_projects_count</span>
<span class="k">FROM</span> <span class="n">projects</span>
<span class="k">WHERE</span> <span class="n">vendor</span> <span class="o">=</span> <span class="o"><</span><span class="n">user_id</span><span class="o">></span> <span class="k">AND</span> <span class="n">status</span> <span class="o">=</span> <span class="s1">'Complete'</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="n">category_id</span>
<span class="p">)</span>
<span class="c1">-- Combine the results</span>
<span class="k">SELECT</span>
<span class="n">s</span><span class="p">.</span><span class="n">category_id</span><span class="p">,</span>
<span class="k">c</span><span class="p">.</span><span class="n">name</span> <span class="k">AS</span> <span class="n">category_name</span><span class="p">,</span>
<span class="n">p</span><span class="p">.</span><span class="n">name</span> <span class="k">AS</span> <span class="n">subcategory_name</span><span class="p">,</span>
<span class="n">COALESCE</span><span class="p">(</span><span class="n">cp</span><span class="p">.</span><span class="n">completed_projects_count</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="k">AS</span> <span class="n">completed_projects_count</span><span class="p">,</span>
<span class="n">COALESCE</span><span class="p">(</span><span class="n">rs</span><span class="p">.</span><span class="n">ratings_count</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="k">AS</span> <span class="n">ratings_count</span><span class="p">,</span>
<span class="n">ROUND</span><span class="p">(</span><span class="n">COALESCE</span><span class="p">(</span><span class="n">rs</span><span class="p">.</span><span class="n">ratings_average</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="mi">1</span><span class="p">)</span> <span class="k">AS</span> <span class="n">ratings_average</span>
<span class="k">FROM</span> <span class="n">services</span> <span class="n">s</span>
<span class="k">JOIN</span> <span class="n">categories</span> <span class="k">c</span> <span class="k">ON</span> <span class="k">c</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">category_id</span>
<span class="k">JOIN</span> <span class="n">categories</span> <span class="n">p</span> <span class="k">ON</span> <span class="n">p</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="k">c</span><span class="p">.</span><span class="n">parent_id</span>
<span class="k">LEFT</span> <span class="k">JOIN</span> <span class="n">rating_summary</span> <span class="n">rs</span> <span class="k">ON</span> <span class="n">rs</span><span class="p">.</span><span class="n">category_id</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">category_id</span>
<span class="k">LEFT</span> <span class="k">JOIN</span> <span class="n">completed_projects</span> <span class="n">cp</span> <span class="k">ON</span> <span class="n">cp</span><span class="p">.</span><span class="n">category_id</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">category_id</span>
<span class="k">WHERE</span> <span class="n">s</span><span class="p">.</span><span class="k">user</span> <span class="o">=</span> <span class="o"><</span><span class="n">user_id</span><span class="o">></span> <span class="k">AND</span> <span class="n">s</span><span class="p">.</span><span class="n">status</span> <span class="o">=</span> <span class="s1">'approved'</span> <span class="k">AND</span> <span class="n">s</span><span class="p">.</span><span class="n">active</span> <span class="o">=</span> <span class="k">true</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">completed_projects_count</span> <span class="k">DESC</span><span class="p">;</span>
</code></pre></div></div>
<p>While the AI results are good, it’s still doubtful who and how to use the tool. Not seasoned developers might don’t understand where the bottleneck is. They would struggle with finding a correct question for ChatGPT. They would not understand how to fix the generated code if it has bugs. Later, I tried to repeat the experiment. This time the machine produced a very different SQL:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Warming up --------------------------------------
ruby 1.000 i/100ms
sql 52.000 i/100ms
sql_gpt 40.000 i/100ms
sql_gpt_new 38.000 i/100ms
Calculating -------------------------------------
ruby 5.754 (±17.4%) i/s - 28.000 in 5.039290s
sql 433.067 (±20.3%) i/s - 2.080k in 5.029710s
sql_gpt 561.018 (±14.4%) i/s - 2.760k in 5.044415s
sql_gpt_new 514.242 (±19.4%) i/s - 2.432k in 5.013023s
Comparison:
sql_gpt: 561.0 i/s
sql_gpt_new: 514.2 i/s - same-ish: difference falls within error
sql: 433.1 i/s - same-ish: difference falls within error
ruby: 5.8 i/s - 97.50x slower
</code></pre></div></div>
<p>However, it’s still faster than the original rewrite to SQL.</p>
<h2 id="conclusion">Conclusion</h2>
<p>ChatGPT is very good at code refactoring and transforming Ruby code into SQL. Even though the results are impressive it still needs an expert communicating with the tool to form a correct question, check the produced results, and fix minor issues. The results can be a good start in code refactoring and optimization.</p>
<p>That was a great meeting. We had a memorable time together. Thanks to everyone who participated. I am looking forward to our weekly calls and I invite everyone to join the community.</p>
<p>Happy coding!</p>
<p><a href="http://blog.widefix.com/optimize-performance-of-rails-app-with-chatgpt/">Optimize Rails app performance with ChatGPT</a> was originally published by Andrei Kaleshka at <a href="http://blog.widefix.com">Take your Rails development to the next level</a> on June 07, 2023.</p>http://blog.widefix.com/boost-your-business-with-expert-ruby-on-rails-development-consultancy2023-05-31 13:43:33 +0200T00:00:00-00:002023-05-31T00:00:00-04:00Andrei Kaleshkahttp://blog.widefix.comka8725@gmail.com
<p>Nowadays, a robust and efficient web application is crucial for businesses. Ruby on Rails (RoR), is a popular and powerful web development framework. It can help transform your ideas into scalable and fast applications. Yet, RoR development can be challenging without the right expertise. A specialized Ruby on Rails development consultancy can help you with that. It provides you with guidance and support. This article explores the benefits and aspects of outsourcing Ruby on Rails development.</p>
<h2 id="unleash-the-power-of-ruby-on-rails">Unleash the Power of Ruby on Rails</h2>
<p>Ruby on Rails is made for productivity and streamlining the web development process. A skilled consultancy leverages the full potential of RoR. It utilizes its robust features, conventions, and best practices. It ensures the development is scalable and maintainable. It adds feature-rich web applications that cater to your business requirements.</p>
<h2 id="custom-tailored-solutions">Custom-Tailored Solutions</h2>
<p>A Ruby on Rails development consultancy understands that business is unique. It does the following things:</p>
<ul>
<li>Works closely with you to analyze your requirements.</li>
<li>Creates a comprehensive development roadmap.</li>
<li>Designs a customized solution that aligns with your business objectives.</li>
<li>Brainstorms ideas to craft a user-friendly interface.</li>
<li>Ensures your web application stands out.</li>
</ul>
<h2 id="expertise-and-experience">Expertise and Experience</h2>
<p>A Ruby on Rails development consultancy supplies you with an experienced professional team. Their extensive knowledge and expertise allow them to solve complex challenges. They can optimize performance and deliver high-quality results. They stay up-to-date with the latest trends and tools. It ensures your application remains cutting-edge and future-proof.</p>
<h2 id="faster-time-to-market">Faster Time to Market</h2>
<p>In today’s evolving market, speed is essential. With a Ruby on Rails consultancy, you can speed up your development process and get your web application to market faster. Their agile development approach enables rapid prototyping and iterative development. Integration of new features becomes seamless. It allows you to capitalize on opportunities.</p>
<h2 id="ongoing-support-and-maintenance">Ongoing Support and Maintenance</h2>
<p>Building a web application is the beginning. A reputable Ruby on Rails development consultancy provides post-development support and maintenance services. They ensure your application remains secure, stable, and optimized for performance. From fixing bugs to implementing updates and enhancements. Their dedicated support team ensures to deliver a seamless user experience.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Partnering with a specialized Ruby on Rails development consultancy offers many advantages. From harnessing the power of RoR to receiving custom-tailored solutions and ongoing support. Their expertise and experience will elevate your development process. You speed up your time to market. You boost your business and stay ahead of the competition in the digital landscape.</p>
<p><a href="http://blog.widefix.com/boost-your-business-with-expert-ruby-on-rails-development-consultancy/">Boost Your Business with Expert Ruby on Rails Development Consultancy</a> was originally published by Andrei Kaleshka at <a href="http://blog.widefix.com">Take your Rails development to the next level</a> on May 31, 2023.</p>http://blog.widefix.com/how-to-write-sql-query-in-ruby-on-rails2023-05-25 11:49:18 +0200T00:00:00-00:002023-05-25T00:00:00-04:00Andrei Kaleshkahttp://blog.widefix.comka8725@gmail.com
<p>ActiveRecord is a central component of a Rails application. It’s a wrapper for a database used behind the scene. And the database is another essential thing in almost any web application. Understanding how these two things go along is crucial to develop a fast, performant, and correct project. Missing knowledge here leads to overengineered solutions, performance issues, and bad user experience.</p>
<p>In this article, you will see how to write SQL and use that knowledge in a Rails application.</p>
<h2 id="how-to-see-which-sql-generated-by-activerecord">How to see which SQL generated by ActiveRecord</h2>
<blockquote>
<p>The post uses this <a href="https://github.com/widefix/demo-fast-sql" ref="nofollow" target="_blank">app</a> to play with examples.</p>
</blockquote>
<p>To see which SQL is generated by queries to models in a common Rails app, use <code class="highlighter-rouge">.to_sql</code> method. We can check that in the Rails console (run <code class="highlighter-rouge">rails console</code> and then write your experimental Ruby code there):</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">></span> <span class="no">Project</span><span class="p">.</span><span class="nf">all</span><span class="p">.</span><span class="nf">to_sql</span>
<span class="o">=></span> <span class="s2">"SELECT </span><span class="se">\"</span><span class="s2">projects</span><span class="se">\"</span><span class="s2">.* FROM </span><span class="se">\"</span><span class="s2">projects</span><span class="se">\"</span><span class="s2">"</span>
</code></pre></div></div>
<p>Note, it works for any query, for example, when it uses conditions or joins:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">></span> <span class="no">Project</span><span class="p">.</span><span class="nf">joins</span><span class="p">(</span><span class="ss">:ratings</span><span class="p">).</span><span class="nf">to_sql</span>
<span class="o">=></span> <span class="s2">"SELECT </span><span class="se">\"</span><span class="s2">projects</span><span class="se">\"</span><span class="s2">.* FROM </span><span class="se">\"</span><span class="s2">projects</span><span class="se">\"</span><span class="s2"> INNER JOIN </span><span class="se">\"</span><span class="s2">ratings</span><span class="se">\"</span><span class="s2"> ON </span><span class="se">\"</span><span class="s2">ratings</span><span class="se">\"</span><span class="s2">.</span><span class="se">\"</span><span class="s2">project_id</span><span class="se">\"</span><span class="s2"> = </span><span class="se">\"</span><span class="s2">projects</span><span class="se">\"</span><span class="s2">.</span><span class="se">\"</span><span class="s2">id</span><span class="se">\"</span><span class="s2">"</span>
<span class="o">></span> <span class="no">Project</span><span class="p">.</span><span class="nf">joins</span><span class="p">(</span><span class="ss">:ratings</span><span class="p">).</span><span class="nf">where</span><span class="p">(</span><span class="ss">ratings: </span><span class="p">{</span><span class="ss">rating: </span><span class="mi">5</span><span class="p">}).</span><span class="nf">to_sql</span>
<span class="o">=></span> <span class="s2">"SELECT </span><span class="se">\"</span><span class="s2">projects</span><span class="se">\"</span><span class="s2">.* FROM </span><span class="se">\"</span><span class="s2">projects</span><span class="se">\"</span><span class="s2"> INNER JOIN </span><span class="se">\"</span><span class="s2">ratings</span><span class="se">\"</span><span class="s2"> ON </span><span class="se">\"</span><span class="s2">ratings</span><span class="se">\"</span><span class="s2">.</span><span class="se">\"</span><span class="s2">project_id</span><span class="se">\"</span><span class="s2"> = </span><span class="se">\"</span><span class="s2">projects</span><span class="se">\"</span><span class="s2">.</span><span class="se">\"</span><span class="s2">id</span><span class="se">\"</span><span class="s2"> WHERE </span><span class="se">\"</span><span class="s2">ratings</span><span class="se">\"</span><span class="s2">.</span><span class="se">\"</span><span class="s2">rating</span><span class="se">\"</span><span class="s2"> = 5"</span>
</code></pre></div></div>
<p>The query can be very complex. It can be constructed from different parts of the app using scopes, and separate classes implementing some patterns like Query Object, etc. In the end, the generated SQL query defines a related feature correctness. Hence, use this trick while debugging code to find a bug in the generated query.</p>
<h2 id="how-to-write-raw-sql-in-a-rails-application">How to write raw SQL in a Rails application</h2>
<p>ActiveRecord comes with a handy interface and abstraction to write raw SQL queries. Why should one do that? Well, there can be many reasons for that:</p>
<ul>
<li>Improving query performance.</li>
<li>Lack of knowledge in ActiveRecord.</li>
<li>Lack of functionalities in ActiveRecord.</li>
<li>Avoiding Arel queries (this thing is used behind the scene of ActiveRecord to generate SQL queries out of the Ruby constructions).</li>
</ul>
<p>Whatever the reason, it’s reduced to just one desire - <strong>demand of controlling SQL queries</strong>. If you are one of this kind of person having this desire, use the following approach:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">></span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">connection</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="s2">"select * from projects"</span><span class="p">).</span><span class="nf">to_a</span>
<span class="o">=></span> <span class="p">[</span>
<span class="p">{</span><span class="s2">"id"</span><span class="o">=></span><span class="mi">1</span><span class="p">,</span>
<span class="s2">"description"</span><span class="o">=></span><span class="s2">"it's a test project"</span><span class="p">,</span>
<span class="s2">"user_id"</span><span class="o">=></span><span class="mi">3</span><span class="p">,</span>
<span class="s2">"created_at"</span><span class="o">=></span><span class="mi">2022</span><span class="o">-</span><span class="mi">09</span><span class="o">-</span><span class="mi">29</span> <span class="mi">20</span><span class="p">:</span><span class="mi">10</span><span class="p">:</span><span class="mf">46.751835</span> <span class="no">UTC</span><span class="p">,</span>
<span class="s2">"updated_at"</span><span class="o">=></span><span class="mi">2023</span><span class="o">-</span><span class="mo">01</span><span class="o">-</span><span class="mi">17</span> <span class="mi">23</span><span class="p">:</span><span class="mi">22</span><span class="p">:</span><span class="mf">42.275697</span> <span class="no">UTC</span><span class="p">,</span>
<span class="s2">"status"</span><span class="o">=></span><span class="s2">"Open"</span><span class="p">,</span>
<span class="s2">"category"</span><span class="o">=></span><span class="s2">"Website and landing page design"</span><span class="p">,</span>
<span class="s2">"experience"</span><span class="o">=></span><span class="s2">"New to Widefix — under one year"</span><span class="p">,</span>
<span class="s2">"existing_website"</span><span class="o">=></span><span class="s2">"Yes, I already have a website set up and live for customers"</span><span class="p">,</span>
<span class="s2">"existing_website_platform"</span><span class="o">=></span><span class="s2">"Widefix"</span><span class="p">,</span>
<span class="s2">"category_tasks"</span><span class="o">=></span><span class="kp">nil</span><span class="p">,</span>
<span class="s2">"delivery_timeline"</span><span class="o">=></span><span class="s2">"11-21 days"</span><span class="p">,</span>
<span class="o">...</span>
<span class="p">},</span> <span class="o">...</span><span class="p">]</span>
</code></pre></div></div>
<p>Also, this API allows to write SQL queries inside migrations. It’s even easier in migrations as the <code class="highlighter-rouge">execute</code> method is available there.</p>
<p>To construct correct SQL you would propaply need a db console. It’s the fastest way to write pure and correct SQL related to your Rails app DB. Use <code class="highlighter-rouge">rails db</code> command to open DB console and do your experiments with SQL.</p>
<h2 id="how-to-measure-sql-query-performance-in-rails">How to measure SQL query performance in Rails</h2>
<p>To understand why SQL query is slow you can use explain analyze built-in functionality that every SQL DB has:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Project</span><span class="p">.</span><span class="nf">joins</span><span class="p">(</span><span class="ss">:ratings</span><span class="p">).</span><span class="nf">where</span><span class="p">(</span><span class="ss">ratings: </span><span class="p">{</span><span class="ss">rating: </span><span class="mi">5</span><span class="p">}).</span><span class="nf">explain</span>
<span class="no">Project</span> <span class="no">Load</span> <span class="p">(</span><span class="mf">2.1</span><span class="n">ms</span><span class="p">)</span> <span class="no">SELECT</span> <span class="s2">"projects"</span><span class="p">.</span><span class="nf">*</span> <span class="no">FROM</span> <span class="s2">"projects"</span> <span class="no">INNER</span> <span class="no">JOIN</span> <span class="s2">"ratings"</span> <span class="no">ON</span> <span class="s2">"ratings"</span><span class="o">.</span><span class="s2">"project_id"</span> <span class="o">=</span> <span class="s2">"projects"</span><span class="o">.</span><span class="s2">"id"</span> <span class="no">WHERE</span> <span class="s2">"ratings"</span><span class="o">.</span><span class="s2">"rating"</span> <span class="o">=</span> <span class="vg">$1</span> <span class="p">[[</span><span class="s2">"rating"</span><span class="p">,</span> <span class="mi">5</span><span class="p">]]</span>
<span class="o">=></span>
<span class="no">EXPLAIN</span> <span class="ss">for: </span><span class="no">SELECT</span> <span class="s2">"projects"</span><span class="p">.</span><span class="nf">*</span> <span class="no">FROM</span> <span class="s2">"projects"</span> <span class="no">INNER</span> <span class="no">JOIN</span> <span class="s2">"ratings"</span> <span class="no">ON</span> <span class="s2">"ratings"</span><span class="o">.</span><span class="s2">"project_id"</span> <span class="o">=</span> <span class="s2">"projects"</span><span class="o">.</span><span class="s2">"id"</span> <span class="no">WHERE</span> <span class="s2">"ratings"</span><span class="o">.</span><span class="s2">"rating"</span> <span class="o">=</span> <span class="vg">$1</span> <span class="p">[[</span><span class="s2">"rating"</span><span class="p">,</span> <span class="mi">5</span><span class="p">]]</span>
<span class="no">QUERY</span> <span class="no">PLAN</span>
<span class="o">---------------------------------------------------------------------------------------</span>
<span class="no">Nested</span> <span class="no">Loop</span> <span class="p">(</span><span class="n">cost</span><span class="o">=</span><span class="mf">0.14</span><span class="o">..</span><span class="mf">9.63</span> <span class="n">rows</span><span class="o">=</span><span class="mi">1</span> <span class="n">width</span><span class="o">=</span><span class="mi">1694</span><span class="p">)</span>
<span class="o">-></span> <span class="no">Seq</span> <span class="no">Scan</span> <span class="n">on</span> <span class="n">ratings</span> <span class="p">(</span><span class="n">cost</span><span class="o">=</span><span class="mf">0.00</span><span class="o">..</span><span class="mf">1.29</span> <span class="n">rows</span><span class="o">=</span><span class="mi">1</span> <span class="n">width</span><span class="o">=</span><span class="mi">8</span><span class="p">)</span>
<span class="no">Filter</span><span class="p">:</span> <span class="p">(</span><span class="n">rating</span> <span class="o">=</span> <span class="mi">5</span><span class="p">)</span>
<span class="o">-></span> <span class="no">Index</span> <span class="no">Scan</span> <span class="n">using</span> <span class="n">projects_pkey</span> <span class="n">on</span> <span class="n">projects</span> <span class="p">(</span><span class="n">cost</span><span class="o">=</span><span class="mf">0.14</span><span class="o">..</span><span class="mf">8.16</span> <span class="n">rows</span><span class="o">=</span><span class="mi">1</span> <span class="n">width</span><span class="o">=</span><span class="mi">1694</span><span class="p">)</span>
<span class="no">Index</span> <span class="no">Cond</span><span class="p">:</span> <span class="p">(</span><span class="nb">id</span> <span class="o">=</span> <span class="n">ratings</span><span class="p">.</span><span class="nf">project_id</span><span class="p">)</span>
<span class="p">(</span><span class="mi">5</span> <span class="n">rows</span><span class="p">)</span>
</code></pre></div></div>
<p>If the query was slow, we had to pay attention to the <code class="highlighter-rouge">Seq Scan on ratings</code>. That means the it didn’t use index to filter ratings by the <code class="highlighter-rouge">rating = 5</code> condition. We can add a corresponding index to fix the issue.</p>
<p>Another problem that is frequently related to performance penalties is N+1 queries. Why many solutions can help with that, it’s pretty easy to detect the issues by looking into logs. Many repeated lines with the following format indicate that issue:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="no">Category</span> <span class="no">Load</span> <span class="p">(</span><span class="mf">0.9</span><span class="n">ms</span><span class="p">)</span> <span class="no">SELECT</span> <span class="s2">"categories"</span><span class="p">.</span><span class="nf">*</span> <span class="no">FROM</span> <span class="s2">"categories"</span> <span class="no">WHERE</span> <span class="s2">"categories"</span><span class="o">.</span><span class="s2">"id"</span> <span class="o">=</span> <span class="vg">$1</span> <span class="no">LIMIT</span> <span class="vg">$2</span> <span class="p">[[</span><span class="s2">"id"</span><span class="p">,</span> <span class="mi">3</span><span class="p">],</span> <span class="p">[</span><span class="s2">"LIMIT"</span><span class="p">,</span> <span class="mi">1</span><span class="p">]]</span>
<span class="err">↳</span> <span class="n">app</span><span class="o">/</span><span class="n">queries</span><span class="o">/</span><span class="n">services_stats_query</span><span class="p">.</span><span class="nf">rb</span><span class="p">:</span><span class="mi">48</span><span class="ss">:in</span> <span class="sb">`block in call_ruby'
Category Load (0.9ms) SELECT "categories".* FROM "categories" WHERE "categories"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
↳ app/queries/services_stats_query.rb:48:in `</span><span class="n">block</span> <span class="k">in</span> <span class="n">call_ruby</span><span class="s1">'
Category Load (0.8ms) SELECT "categories".* FROM "categories" WHERE "categories"."id" = $1 LIMIT $2 [["id", 3], ["LIMIT", 1]]
↳ app/queries/services_stats_query.rb:49:in `block in call_ruby'</span>
<span class="no">Rating</span> <span class="no">Load</span> <span class="p">(</span><span class="mf">0.9</span><span class="n">ms</span><span class="p">)</span> <span class="no">SELECT</span> <span class="s2">"ratings"</span><span class="p">.</span><span class="nf">*</span> <span class="no">FROM</span> <span class="s2">"ratings"</span> <span class="no">WHERE</span> <span class="s2">"ratings"</span><span class="o">.</span><span class="s2">"reviewee_id"</span> <span class="o">=</span> <span class="vg">$1</span> <span class="p">[[</span><span class="s2">"reviewee_id"</span><span class="p">,</span> <span class="mi">10</span><span class="p">]]</span>
<span class="err">↳</span> <span class="n">app</span><span class="o">/</span><span class="n">queries</span><span class="o">/</span><span class="n">services_stats_query</span><span class="p">.</span><span class="nf">rb</span><span class="p">:</span><span class="mi">37</span><span class="ss">:in</span> <span class="sb">`block in call_ruby'
Project Load (1.0ms) SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2 [["id", 27], ["LIMIT", 1]]
↳ app/queries/services_stats_query.rb:38:in `</span><span class="n">block</span> <span class="p">(</span><span class="mi">2</span> <span class="n">levels</span><span class="p">)</span> <span class="k">in</span> <span class="n">call_ruby</span><span class="s1">'
Project Load (0.9ms) SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2 [["id", 39], ["LIMIT", 1]]
↳ app/queries/services_stats_query.rb:38:in `block (2 levels) in call_ruby'</span>
<span class="no">Project</span> <span class="no">Load</span> <span class="p">(</span><span class="mf">0.9</span><span class="n">ms</span><span class="p">)</span> <span class="no">SELECT</span> <span class="s2">"projects"</span><span class="p">.</span><span class="nf">*</span> <span class="no">FROM</span> <span class="s2">"projects"</span> <span class="no">WHERE</span> <span class="s2">"projects"</span><span class="o">.</span><span class="s2">"id"</span> <span class="o">=</span> <span class="vg">$1</span> <span class="no">LIMIT</span> <span class="vg">$2</span> <span class="p">[[</span><span class="s2">"id"</span><span class="p">,</span> <span class="mi">86</span><span class="p">],</span> <span class="p">[</span><span class="s2">"LIMIT"</span><span class="p">,</span> <span class="mi">1</span><span class="p">]]</span>
</code></pre></div></div>
<p>Usually, eager loading techniques fix this issue. But that’s a topic for a separate article.</p>
<h2 id="play-with-sql-in-rails-apps-with-confidence">Play with SQL in Rails apps with confidence</h2>
<p>Now you know all the essential tricks to work with SQL in a Rails application.</p>
<p>Moving further, you can read the following articles that expand the related knowledge:</p>
<ul>
<li><a href="https:http://blog.widefix.com/importance-sql-for-rails-experts/" ref="nofollow" target="_blank">Make your Ruby on Rails app 80x faster with SQL</a></li>
<li><a href="https:http://blog.widefix.com/financial-plan-on-postgresql/" ref="nofollow" target="_blank">Financial plan on PostgreSQL</a></li>
<li><a href="https:http://blog.widefix.com/financial-plan-on-rails/" ref="nofollow" target="_blank">Financial plan on Rails</a></li>
<li><a href="https:http://blog.widefix.com/from-single-dd-to-multiple-checkboxes/" ref="nofollow" target="_blank">From Single drop-down to Multiple check-boxes</a></li>
<li><a href="https:http://blog.widefix.com/date-ranges-overlap/" ref="nofollow" target="_blank">Efficient algorithm to check dates overlap</a></li>
</ul>
<p>These articles show how to use SQL within Rails applications efficiently on practical examples. They also explain some useful but not very popular in the Rails community SQL tricks.</p>
<p>Happy coding!</p>
<p><a href="http://blog.widefix.com/how-to-write-sql-query-in-ruby-on-rails/">How to write SQL query in Ruby On Rails</a> was originally published by Andrei Kaleshka at <a href="http://blog.widefix.com">Take your Rails development to the next level</a> on May 25, 2023.</p>http://blog.widefix.com/improve-nextjs-application-performance2023-05-24 18:23:56 +0200T00:00:00-00:002023-05-24T00:00:00-04:00Andrei Kaleshkahttp://blog.widefix.comka8725@gmail.com
<p>To improve the performance of your app built on React and Next.js using TypeScript, you can follow several strategies.</p>
<h2 id="code-optimization">Code Optimization</h2>
<p>Minimize unnecessary re-renders by using <code class="highlighter-rouge">React.memo</code>, <code class="highlighter-rouge">useMemo</code>, and <code class="highlighter-rouge">useCallback</code> hooks to memoize components and functions that don’t depend on changing data.
Avoid unnecessary state updates and re-renders by optimizing the usage of useState and <code class="highlighter-rouge">useEffect</code> hooks.</p>
<p>Using TypeScript’s type checking can catch potential errors and improve code quality.</p>
<h2 id="bundle-size-optimization">Bundle Size Optimization</h2>
<p>Split your code into smaller chunks using dynamic imports and code splitting. This allows you to load only the necessary code for each page or component, reducing the initial bundle size and improving load times.
Analyze your bundle using tools like Webpack Bundle Analyzer to identify and eliminate any unnecessary dependencies or large libraries.
Compress and optimize your assets (images, CSS, etc.) to reduce their size and improve load times. Next.js has built-in <code class="highlighter-rouge">Image</code> component that allows to compress images on the fly. It also converts then to modern image formats that are lightweight.</p>
<h2 id="server-side-rendering-ssr-and-static-site-generation-ssg">Server-Side Rendering (SSR) and Static Site Generation (SSG)</h2>
<p>Utilize Next.js’s built-in features for server-side rendering and static site generation to pre-render pages and improve initial load times. This reduces the amount of work required by the client’s browser.
Identify pages that don’t require real-time data and generate them statically using Next.js’s <code class="highlighter-rouge">getStaticProps</code> or <code class="highlighter-rouge">getStaticPaths</code> functions. This eliminates the need for client-side rendering and improves performance.</p>
<h2 id="performance-monitoring-and-optimization">Performance Monitoring and Optimization</h2>
<p>Use performance monitoring tools like <code class="highlighter-rouge">Lighthouse</code>, <code class="highlighter-rouge">WebPageTest</code>, or Chrome DevTools to identify performance bottlenecks, such as slow-loading components, large files, or long JavaScript execution times.
Optimize critical rendering paths by prioritizing the loading of essential content and deferring non-critical scripts or styles.
Implement lazy loading for images and other non-essential assets to load them only when they become visible in the viewport.
Implement caching mechanisms for API requests or frequently accessed data to reduce server load and improve response times.</p>
<h2 id="ssr-caching-and-incremental-static-regeneration">SSR Caching and Incremental Static Regeneration</h2>
<p>Utilize Next.js’s caching capabilities to cache rendered pages on the server-side and serve them directly for subsequent requests, reducing the need for re-rendering.
Implement incremental static regeneration for pages that require dynamic data. This allows you to re-generate and update specific pages at predefined intervals, ensuring the content stays fresh while reducing the load on the server.</p>
<h2 id="performance-testing-and-optimization-iteration">Performance Testing and Optimization Iteration</h2>
<p>Regularly test your app’s performance using tools like Lighthouse or Chrome DevTools to measure metrics like First Contentful Paint (FCP), Time to Interactive (TTI), and Total Blocking Time (TBT). Set performance budgets and strive to stay within them.
Continuously analyze and optimize critical paths, reducing the time required for JavaScript execution, network requests, and rendering.
Remember to always profile and benchmark your optimizations to ensure they have a positive impact on your app’s performance.</p>
<p><a href="http://blog.widefix.com/improve-nextjs-application-performance/">Improve NextJS application performance</a> was originally published by Andrei Kaleshka at <a href="http://blog.widefix.com">Take your Rails development to the next level</a> on May 24, 2023.</p>http://blog.widefix.com/choosing-top-tier-ruby-on-rails-consulting2023-05-23 13:47:19 +0200T00:00:00-00:002023-05-23T00:00:00-04:00Andrei Kaleshkahttp://blog.widefix.comka8725@gmail.com
<p>Ruby on Rails has long been recognized as a powerful web development framework, enabling developers to build robust and scalable applications with ease. However, navigating the vast landscape of Ruby on Rails development can be a daunting task, especially when faced with complex projects and tight deadlines. This is where top-tier Ruby on Rails consulting services come into play, offering invaluable expertise, guidance, and support. In this article, we explore the benefits of engaging with renowned Ruby on Rails consulting firms, shedding light on why partnering with the best can make all the difference.</p>
<h2 id="leveraging-deep-domain-expertise">Leveraging Deep Domain Expertise</h2>
<p>Top-tier Ruby on Rails consulting firms boast teams of skilled professionals. They have honed their expertise through years of hands-on experience. These experts have an in-depth understanding of Ruby on Rails and its ecosystem. That allows them to navigate complex challenges. By leveraging their domain expertise, these consultants can provide invaluable insights. They can recommend best practices and suggest innovative solutions to your project specifics. Their guidance ensures that your application is efficient, scalable, and maintainable.</p>
<h2 id="accelerating-development-time">Accelerating Development Time</h2>
<p>Time is of the essence in today’s fast-paced business environment. Partner with a Ruby on Rails consulting firm to hurry your project’s timeline. But make sure you choose a reputable consulting firm. These firms are adept at optimizing development workflows. They leverage reusable code libraries to ensure rapid development cycles. With their expertise, you can deliver high-quality applications within tight deadlines. With that, you gain a competitive edge in the market.</p>
<h2 id="ensuring-scalability-and-performance">Ensuring Scalability and Performance</h2>
<p>Building a scalable and high-performing application is crucial for long-term success. Ruby on Rails consulting firms understand the intricacies of scaling applications. That handles growing user bases and increasing traffic. They use proven techniques and architectural patterns. That ensures your application can handle heavy loads. They incorporate strategies such as caching, load balancing, and database optimization. They ensure your application can scale as your business grows. They provide a smooth user experience and avoid potential bottlenecks.</p>
<h2 id="security-and-risk-mitigation">Security and Risk Mitigation</h2>
<p>Data security and risk mitigation are crucial concerns for any business operating. Renowned Ruby on Rails consulting firms focus on robust security practices. They have extensive experience in implementing industry-standard security measures. They can conduct comprehensive security audits. They can identify vulnerabilities and put in place stringent security protocols. All that is to safeguard your application and sensitive data. By mitigating risks, these experts help you build trust with your users. That protects your brand reputation.</p>
<h2 id="continued-support-and-maintenance">Continued Support and Maintenance</h2>
<p>Launching your Ruby on Rails application is the beginning. Ongoing support and maintenance are crucial. A good consulting firm ensures smooth operation and addresses any emerging issues. Top-tier consulting firms provide reliable post-launch support. They offer timely bug fixes, performance optimizations, and feature enhancements. At the same time, you can rest. You know that your application will remain up-to-date, secure, and functional. That allows you to focus on your core business activities.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Nowadays, choosing the right Ruby on Rails consulting partner is vital for success. By engaging with top-tier consulting firms, you can leverage their deep domain expertise. You speed up development time. Your project gets scalable and performant. You have risks mitigated and receive continued support. The benefits —higher-quality applications, reduced time-to-market, enhanced security, and peace of mind. Be sure to partner with a reputable consulting firm that can unlock the full potential of your application.</p>
<p><a href="http://blog.widefix.com/choosing-top-tier-ruby-on-rails-consulting/">Choosing Top-Tier Ruby on Rails Consulting</a> was originally published by Andrei Kaleshka at <a href="http://blog.widefix.com">Take your Rails development to the next level</a> on May 23, 2023.</p>http://blog.widefix.com/importance-sql-for-rails-experts2023-03-30 11:59:17 +0200T00:00:00-00:002023-03-30T00:00:00-04:00Andrei Kaleshkahttp://blog.widefix.comka8725@gmail.com
<p>SQL can improve the performance and efficiency of your Ruby on Rails application. No need for heavy technologies. No need for switching to another programming language or framework.</p>
<p>SQL is a powerful tool that can take your Ruby on Rails expertise to the next level. Nowadays many technologies claim to replace SQL or deem Ruby on Rails as “too slow”. But it remains a popular and efficient choice for many web projects. This article will explore the importance of SQL for Ruby on Rails experts. It will show why it’s a must-have skill in today’s development landscape.</p>
<h2 id="why-ruby-on-rails-can-be-slow">Why Ruby on Rails can be slow</h2>
<p>Imagine the following class that’s used in a Rails controller to feed data to a page in your application. This class may look familiar to you. I’ve encountered similar classes in real-world applications:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ServicesStatsQuery</span>
<span class="nb">attr_accessor</span> <span class="ss">:user</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">user</span> <span class="o">=</span> <span class="n">user</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">call</span>
<span class="n">projects_full</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">projects_empty</span> <span class="o">=</span> <span class="p">[]</span>
<span class="no">Service</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">user: </span><span class="n">user</span><span class="p">,</span> <span class="ss">status: </span><span class="s2">"approved"</span><span class="p">,</span> <span class="ss">active: </span><span class="kp">true</span><span class="p">)</span>
<span class="p">.</span><span class="nf">order</span><span class="p">(</span><span class="ss">category_id: :asc</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">service</span><span class="o">|</span>
<span class="n">ratings_average</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">ratings_count</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">ratings_total</span> <span class="o">=</span> <span class="mi">0</span>
<span class="no">Rating</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">reviewee: </span><span class="n">user</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">rating</span><span class="o">|</span>
<span class="n">project</span> <span class="o">=</span> <span class="no">Project</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">rating</span><span class="p">.</span><span class="nf">project_id</span><span class="p">)</span>
<span class="k">if</span> <span class="n">project</span><span class="p">.</span><span class="nf">category_id</span> <span class="o">==</span> <span class="n">service</span><span class="p">.</span><span class="nf">category_id</span>
<span class="n">ratings_count</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="n">ratings_total</span> <span class="o">+=</span> <span class="n">rating</span><span class="p">.</span><span class="nf">rating</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">ratings_average</span> <span class="o">=</span> <span class="p">(</span><span class="n">ratings_total</span> <span class="o">/</span> <span class="n">ratings_count</span><span class="p">.</span><span class="nf">to_f</span><span class="p">).</span><span class="nf">round</span><span class="p">(</span><span class="mi">1</span><span class="p">).</span><span class="nf">to_s</span> <span class="k">if</span> <span class="n">ratings_count</span> <span class="o">!=</span> <span class="mi">0</span> <span class="o">&&</span> <span class="n">ratings_total</span> <span class="o">!=</span> <span class="mi">0</span>
<span class="n">completed_projects_count</span> <span class="o">=</span>
<span class="no">Project</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">vendor: </span><span class="n">user</span><span class="p">,</span> <span class="ss">status: </span><span class="s2">"Complete"</span><span class="p">,</span> <span class="ss">category_id: </span><span class="n">service</span><span class="p">.</span><span class="nf">category_id</span><span class="p">).</span><span class="nf">size</span>
<span class="n">service_hash</span> <span class="o">=</span> <span class="p">{</span>
<span class="ss">category_id: </span><span class="n">service</span><span class="p">.</span><span class="nf">category_id</span><span class="p">,</span>
<span class="ss">category_name: </span><span class="no">Category</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="no">Category</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">service</span><span class="p">.</span><span class="nf">category_id</span><span class="p">).</span><span class="nf">parent_id</span><span class="p">).</span><span class="nf">name</span><span class="p">,</span>
<span class="ss">subcategory_name: </span><span class="no">Category</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">service</span><span class="p">.</span><span class="nf">category_id</span><span class="p">).</span><span class="nf">name</span><span class="p">,</span>
<span class="ss">completed_projects_count: </span><span class="n">completed_projects_count</span><span class="p">,</span>
<span class="ss">ratings_count: </span><span class="n">ratings_count</span><span class="p">,</span>
<span class="ss">ratings_average: </span><span class="n">ratings_average</span><span class="p">,</span>
<span class="p">}</span>
<span class="n">resolved_hash</span> <span class="o">=</span> <span class="n">service_hash</span><span class="p">.</span><span class="nf">transform_keys</span><span class="p">{</span> <span class="o">|</span><span class="n">k</span><span class="o">|</span> <span class="n">k</span><span class="p">.</span><span class="nf">to_s</span><span class="p">.</span><span class="nf">camelize</span><span class="p">(</span><span class="ss">:lower</span><span class="p">)</span> <span class="p">}</span>
<span class="k">if</span> <span class="n">completed_projects_count</span> <span class="o">></span> <span class="mi">0</span>
<span class="n">projects_full</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="n">resolved_hash</span><span class="p">)</span>
<span class="k">else</span>
<span class="n">projects_empty</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="n">resolved_hash</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">projects_full</span><span class="p">.</span><span class="nf">sort_by!</span><span class="p">{</span> <span class="o">|</span><span class="n">k</span><span class="o">|</span> <span class="o">-</span><span class="n">k</span><span class="p">[</span><span class="s2">"completedProjectsCount"</span><span class="p">]</span> <span class="p">}</span>
<span class="n">projects_full</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="o">*</span><span class="n">projects_empty</span><span class="p">)</span>
<span class="n">projects_full</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Your application is running in production and serving the needs of hundreds of users. You’ve noticed that the related page is becoming slower and slower with each passing day. This can be a frustrating problem to solve. It’s natural to feel tempted to consider drastic measures. You can think of switching to a faster programming language. Or you might plan to use heavy technology.</p>
<p>Before taking such drastic steps take a step back and understand the root cause of the issue.</p>
<p>There are a few things that could be contributing to the slowness of this code:</p>
<ul>
<li>
<p>N+1 queries: The code is making a separate database query for each rating. Then it gets the associated project. That can become slow if there are a large number of ratings.</p>
</li>
<li>
<p>Nested <code class="highlighter-rouge">each</code> loops: The code is using this method to iterate over the Service. For each service in the nested loop, it iterates through all related Rating records. That can also be slow if there are many records. Instead, it would be better to use SQL joins to fetch all the data in a single query.</p>
</li>
<li>
<p>Redundant queries: The code is calling <code class="highlighter-rouge">Category.find</code> many times. Instead, better to fetch all the category data using a single query. And then use it to populate the <code class="highlighter-rouge">service_hash</code> object.</p>
</li>
</ul>
<p>Optimizing this code will need a combination of SQL query optimization. That should cut unnecessary database queries and improve the performance of the code.</p>
<h2 id="how-to-analyze-slow-ruby-on-rails-code">How to analyze slow Ruby On Rails code</h2>
<p>There are plenty of tools out there, like <a href="https://github.com/flyerhzm/bullet" ref="nofollow" target="_blank">bullet</a>, <a href="https://github.com/evanphx/benchmark-ips" ref="nofollow" target="_blank">benchmark-ips</a>, or any others.</p>
<p>But before taking these radical steps use what you already have. Open logs (use command <code class="highlighter-rouge">tail -f log/development.log</code> for local). Observe what happens there while you open the slow page in your browser. If you spot many repeating lines like this one you have an N+1 problem:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="mo">00</span><span class="p">:</span><span class="mo">03</span><span class="p">:</span><span class="mi">22</span> <span class="n">web</span><span class="o">.</span><span class="mi">1</span> <span class="o">|</span> <span class="no">Rating</span> <span class="no">Load</span> <span class="p">(</span><span class="mf">1.6</span><span class="n">ms</span><span class="p">)</span> <span class="no">SELECT</span> <span class="s2">"ratings"</span><span class="p">.</span><span class="nf">*</span> <span class="no">FROM</span> <span class="s2">"ratings"</span> <span class="no">WHERE</span> <span class="s2">"ratings"</span><span class="o">.</span><span class="s2">"user_id"</span> <span class="o">=</span> <span class="vg">$1</span> <span class="p">[[</span><span class="s2">"user_id"</span><span class="p">,</span> <span class="mi">15</span><span class="p">]]</span>
</code></pre></div></div>
<p>Pay attention to the phrase “Rating Load” as it serves as an indicator. If you notice “CACHE Rating Load”, there’s no need to worry. The object is already in memory, retrieved from there without accessing the database. This suggests that eager loading is already working.</p>
<p>Another tool is to visually inspect the code. You can feel discomfort and uncertainty when trying to understand how the code pieces relate to each other. Yet, this is not related to performance. The most crucial aspect is that the code contains a nested loop, which indicates that its complexity is squared. As more objects are iterated, a squared function grows rapidly:</p>
<p><img src="/images/plot.png" alt="Growth rate for different functions" /></p>
<p>Sorry for getting into the math, but it’s important to understand that your code should ideally grow linearly or at least logarithmically. This is the best way to ensure that your code’s performance remains optimal for many years to come.</p>
<p>If you are unsure about what all of that means, now is a good time to start learning about the theory of algorithms and data structures.</p>
<h2 id="use-sql-to-improve-rails-application-performance">Use SQL to improve Rails application performance</h2>
<p>To fetch all the required data in a single query while performing the necessary aggregation, sorting, and grouping, consider using this SQL query:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">select</span>
<span class="n">s</span><span class="p">.</span><span class="n">category_id</span> <span class="k">as</span> <span class="nv">"categoryId"</span><span class="p">,</span>
<span class="n">pc</span><span class="p">.</span><span class="n">name</span> <span class="k">as</span> <span class="nv">"categoryName"</span><span class="p">,</span>
<span class="k">c</span><span class="p">.</span><span class="n">name</span> <span class="k">as</span> <span class="nv">"subcategoryName"</span><span class="p">,</span>
<span class="k">count</span><span class="p">(</span><span class="n">p</span><span class="p">.</span><span class="n">id</span><span class="p">)</span> <span class="k">as</span> <span class="nv">"completedProjectsCount"</span><span class="p">,</span>
<span class="n">r</span><span class="p">.</span><span class="k">count</span> <span class="k">as</span> <span class="nv">"ratingsCount"</span><span class="p">,</span>
<span class="n">r</span><span class="p">.</span><span class="k">avg</span> <span class="k">as</span> <span class="nv">"ratingsAverage"</span>
<span class="k">from</span> <span class="n">services</span> <span class="n">s</span>
<span class="k">join</span> <span class="n">categories</span> <span class="k">c</span> <span class="k">on</span> <span class="k">c</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">category_id</span>
<span class="k">join</span> <span class="n">categories</span> <span class="n">pc</span> <span class="k">on</span> <span class="n">pc</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="k">c</span><span class="p">.</span><span class="n">parent_id</span>
<span class="k">left</span> <span class="k">join</span> <span class="k">lateral</span> <span class="p">(</span>
<span class="k">select</span> <span class="k">count</span><span class="p">(</span><span class="n">r</span><span class="p">.</span><span class="n">id</span><span class="p">),</span> <span class="k">cast</span><span class="p">(</span><span class="n">round</span><span class="p">(</span><span class="n">coalesce</span><span class="p">(</span><span class="k">avg</span><span class="p">(</span><span class="n">r</span><span class="p">.</span><span class="n">rating</span><span class="p">),</span> <span class="mi">0</span><span class="p">),</span> <span class="mi">1</span><span class="p">)</span> <span class="k">as</span> <span class="nb">text</span><span class="p">)</span> <span class="k">as</span> <span class="k">avg</span> <span class="k">from</span> <span class="n">ratings</span> <span class="n">r</span>
<span class="k">join</span> <span class="n">projects</span> <span class="n">rp</span> <span class="k">on</span> <span class="n">rp</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="n">r</span><span class="p">.</span><span class="n">project_id</span> <span class="k">and</span> <span class="n">s</span><span class="p">.</span><span class="n">category_id</span> <span class="o">=</span> <span class="n">rp</span><span class="p">.</span><span class="n">category_id</span>
<span class="k">where</span> <span class="n">r</span><span class="p">.</span><span class="n">reviewee_id</span> <span class="o">=</span> <span class="p">:</span><span class="n">user_id</span>
<span class="p">)</span> <span class="n">r</span> <span class="k">on</span> <span class="k">true</span>
<span class="k">left</span> <span class="k">join</span> <span class="n">projects</span> <span class="n">p</span> <span class="k">on</span> <span class="n">p</span><span class="p">.</span><span class="n">vendor_id</span> <span class="o">=</span> <span class="p">:</span><span class="n">user_id</span> <span class="k">and</span> <span class="n">p</span><span class="p">.</span><span class="n">status</span> <span class="o">=</span> <span class="s1">'Complete'</span> <span class="k">and</span> <span class="n">p</span><span class="p">.</span><span class="n">category_id</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">category_id</span>
<span class="k">where</span> <span class="n">s</span><span class="p">.</span><span class="n">status</span> <span class="o">=</span> <span class="s1">'approved'</span> <span class="k">and</span> <span class="n">s</span><span class="p">.</span><span class="n">active</span> <span class="k">and</span> <span class="n">s</span><span class="p">.</span><span class="n">user_id</span> <span class="o">=</span> <span class="p">:</span><span class="n">user_id</span>
<span class="k">group</span> <span class="k">by</span> <span class="n">s</span><span class="p">.</span><span class="n">category_id</span><span class="p">,</span> <span class="n">pc</span><span class="p">.</span><span class="n">name</span><span class="p">,</span> <span class="k">c</span><span class="p">.</span><span class="n">name</span><span class="p">,</span> <span class="n">r</span><span class="p">.</span><span class="k">count</span><span class="p">,</span> <span class="n">r</span><span class="p">.</span><span class="k">avg</span>
<span class="k">order</span> <span class="k">by</span> <span class="k">count</span><span class="p">(</span><span class="n">p</span><span class="p">.</span><span class="n">id</span><span class="p">)</span> <span class="k">desc</span><span class="p">,</span> <span class="n">s</span><span class="p">.</span><span class="n">category_id</span> <span class="k">asc</span>
</code></pre></div></div>
<p>One way to test this query is to replace the placeholder <code class="highlighter-rouge">:user_id</code> with an actual user id. Then, we can open the DB console and paste the query to see if it produces the expected results.</p>
<p>Make sure the query is successful and returns the desired results. Then we can replace the slow Ruby code with this efficient SQL query:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ServicesStatsQuery</span>
<span class="no">SQL</span> <span class="o">=</span> <span class="o"><<~</span><span class="no">SQL</span><span class="sh">
select
s.category_id as "categoryId",
pc.name as "categoryName",
c.name as "subcategoryName",
count(p.id) as "completedProjectsCount",
r.count as "ratingsCount",
r.avg as "ratingsAverage"
from services s
join categories c on c.id = s.category_id
join categories pc on pc.id = c.parent_id
left join lateral (
select count(r.id), cast(round(coalesce(avg(r.rating), 0), 1) as text) as avg from ratings r
join projects rp on rp.id = r.project_id and s.category_id = rp.category_id
where r.reviewee_id = :user_id
) r on true
left join projects p on p.vendor_id = :user_id and p.status = 'Complete' and p.category_id = s.category_id
where s.status = 'approved' and s.active and s.user_id = :user_id
group by s.category_id, pc.name, c.name, r.count, r.avg
order by count(p.id) desc, s.category_id asc
</span><span class="no"> SQL</span>
<span class="nb">attr_accessor</span> <span class="ss">:user</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">user</span> <span class="o">=</span> <span class="n">user</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">call</span>
<span class="n">sql</span> <span class="o">=</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">sanitize_sql_array</span><span class="p">([</span><span class="no">SQL</span><span class="p">,</span> <span class="ss">user_id: </span><span class="n">user</span><span class="p">.</span><span class="nf">id</span><span class="p">])</span>
<span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">connection</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="n">sql</span><span class="p">).</span><span class="nf">to_a</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>This SQL query can provide fast results for most Ruby On Rails applications. Although, it assumes that the DB has appropriate indexes defined.</p>
<h2 id="native-sql-is-much-faster-than-the-pure-ruby-on-rails-code">Native SQL is much faster than the pure Ruby on Rails code</h2>
<p>It’s time to perform measurements. First, install the <a href="https://github.com/evanphx/benchmark-ips" ref="nofollow" target="_blank">benchmark-ips</a> gem. Then you can prepare the following code and save it in a file. For example, <code class="highlighter-rouge">t.rb</code>, in the root directory of your Rails application:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'benchmark/ips'</span>
<span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span>
<span class="n">service</span> <span class="o">=</span> <span class="no">ServicesStatsQuery</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="no">Benchmark</span><span class="p">.</span><span class="nf">ips</span> <span class="k">do</span> <span class="o">|</span><span class="n">x</span><span class="o">|</span>
<span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s2">"ruby"</span><span class="p">)</span> <span class="p">{</span> <span class="n">service</span><span class="p">.</span><span class="nf">call_ruby</span> <span class="p">}</span>
<span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s2">"sql"</span><span class="p">)</span> <span class="p">{</span> <span class="n">service</span><span class="p">.</span><span class="nf">call_sql</span> <span class="p">}</span>
<span class="n">x</span><span class="p">.</span><span class="nf">compare!</span>
<span class="k">end</span>
</code></pre></div></div>
<p>We change the <code class="highlighter-rouge">ServicesStatsQuery</code> a bit to have the two methods defined <code class="highlighter-rouge">call_ruby</code> and <code class="highlighter-rouge">call_sql</code> instead of one method <code class="highlighter-rouge">call</code>:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ServicesStatsQuery</span>
<span class="no">SQL</span> <span class="o">=</span> <span class="o"><<~</span><span class="no">SQL</span><span class="sh">
select
s.category_id as "categoryId",
pc.name as "categoryName",
c.name as "subcategoryName",
count(p.id) as "completedProjectsCount",
r.count as "ratingsCount",
r.avg as "ratingsAverage"
from services s
join categories c on c.id = s.category_id
join categories pc on pc.id = c.parent_id
left join lateral (
select count(r.id), cast(round(coalesce(avg(r.rating), 0), 1) as text) as avg from ratings r
join projects rp on rp.id = r.project_id and s.category_id = rp.category_id
where r.reviewee_id = :user_id
) r on true
left join projects p on p.vendor_id = :user_id and p.status = 'Complete' and p.category_id = s.category_id
where s.status = 'approved' and s.active and s.user_id = :user_id
group by s.category_id, pc.name, c.name, r.count, r.avg
order by count(p.id) desc, s.category_id asc
</span><span class="no"> SQL</span>
<span class="nb">attr_accessor</span> <span class="ss">:user</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">user</span> <span class="o">=</span> <span class="n">user</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">call_ruby</span>
<span class="n">projects_full</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">projects_empty</span> <span class="o">=</span> <span class="p">[]</span>
<span class="no">Service</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">user: </span><span class="n">user</span><span class="p">,</span> <span class="ss">status: </span><span class="s2">"approved"</span><span class="p">,</span> <span class="ss">active: </span><span class="kp">true</span><span class="p">).</span><span class="nf">order</span><span class="p">(</span><span class="ss">category_id: :asc</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">service</span><span class="o">|</span>
<span class="n">ratings_average</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">ratings_count</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">ratings_total</span> <span class="o">=</span> <span class="mi">0</span>
<span class="no">Rating</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">reviewee: </span><span class="n">user</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">rating</span><span class="o">|</span>
<span class="n">project</span> <span class="o">=</span> <span class="no">Project</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">rating</span><span class="p">.</span><span class="nf">project_id</span><span class="p">)</span>
<span class="k">if</span> <span class="n">project</span><span class="p">.</span><span class="nf">category_id</span> <span class="o">==</span> <span class="n">service</span><span class="p">.</span><span class="nf">category_id</span>
<span class="n">ratings_count</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="n">ratings_total</span> <span class="o">+=</span> <span class="n">rating</span><span class="p">.</span><span class="nf">rating</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">ratings_average</span> <span class="o">=</span> <span class="p">(</span><span class="n">ratings_total</span> <span class="o">/</span> <span class="n">ratings_count</span><span class="p">.</span><span class="nf">to_f</span><span class="p">).</span><span class="nf">round</span><span class="p">(</span><span class="mi">1</span><span class="p">).</span><span class="nf">to_s</span> <span class="k">if</span> <span class="n">ratings_count</span> <span class="o">!=</span> <span class="mi">0</span> <span class="o">&&</span> <span class="n">ratings_total</span> <span class="o">!=</span> <span class="mi">0</span>
<span class="n">completed_projects_count</span> <span class="o">=</span> <span class="no">Project</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">vendor: </span><span class="n">user</span><span class="p">,</span> <span class="ss">status: </span><span class="s2">"Complete"</span><span class="p">,</span> <span class="ss">category_id: </span><span class="n">service</span><span class="p">.</span><span class="nf">category_id</span><span class="p">).</span><span class="nf">size</span>
<span class="n">service_hash</span> <span class="o">=</span> <span class="p">{</span>
<span class="ss">category_id: </span><span class="n">service</span><span class="p">.</span><span class="nf">category_id</span><span class="p">,</span>
<span class="ss">category_name: </span><span class="no">Category</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="no">Category</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">service</span><span class="p">.</span><span class="nf">category_id</span><span class="p">).</span><span class="nf">parent_id</span><span class="p">).</span><span class="nf">name</span><span class="p">,</span>
<span class="ss">subcategory_name: </span><span class="no">Category</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">service</span><span class="p">.</span><span class="nf">category_id</span><span class="p">).</span><span class="nf">name</span><span class="p">,</span>
<span class="ss">completed_projects_count: </span><span class="n">completed_projects_count</span><span class="p">,</span>
<span class="ss">ratings_count: </span><span class="n">ratings_count</span><span class="p">,</span>
<span class="ss">ratings_average: </span><span class="n">ratings_average</span><span class="p">,</span>
<span class="p">}</span>
<span class="n">resolved_hash</span> <span class="o">=</span> <span class="n">service_hash</span><span class="p">.</span><span class="nf">transform_keys</span><span class="p">{</span> <span class="o">|</span><span class="n">k</span><span class="o">|</span> <span class="n">k</span><span class="p">.</span><span class="nf">to_s</span><span class="p">.</span><span class="nf">camelize</span><span class="p">(</span><span class="ss">:lower</span><span class="p">)</span> <span class="p">}</span>
<span class="k">if</span> <span class="n">completed_projects_count</span> <span class="o">></span> <span class="mi">0</span>
<span class="n">projects_full</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="n">resolved_hash</span><span class="p">)</span>
<span class="k">else</span>
<span class="n">projects_empty</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="n">resolved_hash</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">projects_full</span><span class="p">.</span><span class="nf">sort_by!</span><span class="p">{</span> <span class="o">|</span><span class="n">k</span><span class="o">|</span> <span class="o">-</span><span class="n">k</span><span class="p">[</span><span class="s2">"completedProjectsCount"</span><span class="p">]</span> <span class="p">}</span>
<span class="n">projects_full</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="o">*</span><span class="n">projects_empty</span><span class="p">)</span>
<span class="n">projects_full</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">call_sql</span>
<span class="n">sql</span> <span class="o">=</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">sanitize_sql_array</span><span class="p">([</span><span class="no">SQL</span><span class="p">,</span> <span class="ss">user_id: </span><span class="n">user</span><span class="p">.</span><span class="nf">id</span><span class="p">])</span>
<span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">connection</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span><span class="n">sql</span><span class="p">).</span><span class="nf">to_a</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Run it with the command: <code class="highlighter-rouge">rails runner t.rb</code> and see the results:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Warming</span> <span class="n">up</span> <span class="o">--------------------------------------</span>
<span class="n">ruby</span> <span class="mf">1.000</span> <span class="n">i</span><span class="o">/</span><span class="mi">100</span><span class="n">ms</span>
<span class="n">sql</span> <span class="mf">48.000</span> <span class="n">i</span><span class="o">/</span><span class="mi">100</span><span class="n">ms</span>
<span class="no">Calculating</span> <span class="o">-------------------------------------</span>
<span class="n">ruby</span> <span class="mf">5.658</span> <span class="p">(</span><span class="err">±</span> <span class="mf">0.0</span><span class="o">%</span><span class="p">)</span> <span class="n">i</span><span class="o">/</span><span class="n">s</span> <span class="o">-</span> <span class="mf">29.000</span> <span class="k">in</span> <span class="mf">5.147460</span><span class="n">s</span>
<span class="n">sql</span> <span class="mf">451.144</span> <span class="p">(</span><span class="err">±</span> <span class="mf">8.6</span><span class="o">%</span><span class="p">)</span> <span class="n">i</span><span class="o">/</span><span class="n">s</span> <span class="o">-</span> <span class="mf">2.256</span><span class="n">k</span> <span class="k">in</span> <span class="mf">5.042193</span><span class="n">s</span>
<span class="no">Comparison</span><span class="p">:</span>
<span class="ss">sql: </span><span class="mf">451.1</span> <span class="n">i</span><span class="o">/</span><span class="n">s</span>
<span class="ss">ruby: </span><span class="mf">5.7</span> <span class="n">i</span><span class="o">/</span><span class="n">s</span> <span class="o">-</span> <span class="mf">79.74</span><span class="n">x</span> <span class="n">slower</span>
</code></pre></div></div>
<p>Performance boost of almost 80 times! The slow-loading page that took over 30 seconds now loads within 300ms. This significant improvement is not the programming language or framework merit.</p>
<h2 id="demo-faster-sql-in-ruby-on-rails-app">Demo: Faster SQL in Ruby on Rails App</h2>
<p>I have developed a <a href="https://github.com/widefix/demo-fast-sql" ref="nofollow" target="_blank">demo application using both Ruby and SQL versions</a> and made it available on GitHub as an open source project. This means that you have the opportunity to experiment with the code and run benchmarks on your own.</p>
<h2 id="determine-whether-to-use-the-measurement-tools-or-not">Determine whether to use the measurement tools or not</h2>
<p>You can analyze performance issues and measure optimization results without fancy tools. <a href="https://github.com/evanphx/benchmark-ips" ref="nofollow" target="_blank">benchmark-ips</a> is for demonstration purposes. The best tools are reading the code, and understanding it. Then analyze its complexity. Finally, use SQL knowledge. These tools are enough. Nothing can replace them. Well, except in the distant future, ChatGPT may offer more help.</p>
<p>There are always plenty of built-in tools by your hand. Learn how to use them. For example, moving further, the written SQL can be slow. You will have to check why. Use <a href="https://www.postgresql.org/docs/current/sql-explain.html" ref="nofollow" target="_blank">explain analyze</a> for that. This is a built-in tool in all modern SQL databases.</p>
<h2 id="why-ruby-on-rails-expert-should-know-sql">Why Ruby on Rails expert should know SQL</h2>
<p>Ruby on Rails is a powerful web application framework. It provides developers with high-level abstractions and conventions. That allows to build web applications quickly and efficiently. But, despite its powerful abstractions, Ruby on Rails experts need to know SQL.</p>
<p>SQL allows Ruby on Rails experts to write efficient and optimized database queries. That can improve the performance of a web application. By understanding SQL, a Ruby on Rails expert can design the database schema and data access layer. If that’s done well, the application maximizes performance, scalability, and maintainability.</p>
<p>SQL is a widely-used and powerful language. People use it beyond interacting with the database. Ruby on Rails experts use SQL to perform complex calculations and data transformations. They can generate reports that are challenging to achieve using Ruby code alone.</p>
<p>SQL allows Ruby on Rails experts to have more control over their web applications. They write more efficient and optimized code. They make better data-driven decisions.</p>
<h2 id="how-can-a-ruby-on-rails-expert-learn-sql">How can a Ruby on Rails expert learn SQL</h2>
<p>Nowadays, there are plenty of courses and books available on the subject. So it’s hard to recommend specific resources. You can choose whatever you like and suits your learning style.</p>
<p>But, if you are like me and prefer hands-on learning, you might find the following resource helpful - <a href="https://pgexercises.com/" ref="nofollow" target="_blank">https://pgexercises.com/</a>.</p>
<p>You might also find this article I wrote previously interesting - <a href="https:http://blog.widefix.com/financial-plan-on-postgresql/">Financial plan on PostgreSQL</a>.</p>
<p><a href="http://blog.widefix.com/importance-sql-for-rails-experts/">Make your Ruby on Rails app 80x faster with SQL</a> was originally published by Andrei Kaleshka at <a href="http://blog.widefix.com">Take your Rails development to the next level</a> on March 30, 2023.</p>http://blog.widefix.com/send-emails-with-sendgrid-a-step-by-step-guide-for-ruby-on-rails-applications2023-03-15 15:29:47 +0100T00:00:00-00:002023-03-15T00:00:00-04:00Andrei Kaleshkahttp://blog.widefix.comka8725@gmail.com
<p>Sending emails from a Ruby on Rails application can be a critical aspect of many web applications. Whether it’s for sending password resets, notifications, newsletters or transactional emails, you want your emails to be reliable and fast. That’s where SendGrid comes in. This article guides you through the process of sending emails from your Ruby on Rails application using SendGrid.</p>
<p>Before you start, it’s crucial to understand that username and password authentication for email delivery is no longer sufficient. Nowadays, email provider services like SendGrid require more rigorous verification processes to ensure secure and reliable email delivery. As such, the first step in this journey is setting up your SendGrid account properly.</p>
<h3 id="sendgrid-account-setup">SendGrid Account Setup</h3>
<p>To get started with <a href="https://sendgrid.com/" ref="nofollow" target="_blank">SendGrid</a> in your Ruby on Rails application, you first need to sign up for a SendGrid account and <a href="https://docs.sendgrid.com/ui/account-and-settings/api-keys#creating-an-api-key" ref="nofollow" target="_blank">obtain an API key</a>. Once you have created it, make sure to copy it somewhere. Otherwise, you won’t be able to read it later.</p>
<p>It’s recommended to have <a href="https://docs.sendgrid.com/ui/account-and-settings/how-to-set-up-domain-authentication" ref="nofollow" target="_blank">completed the Domain Authentication process</a> to ensure the deliverability of your emails. This step requires registering your domain, either your own or one provided by other services such as Heroku that generate it automatically. By authenticating your domain, you can verify that you are a legitimate sender, improve email deliverability, and reduce the risk of your emails being flagged as spam.</p>
<p>When using SendGrid to send emails from your Ruby on Rails application, it’s essential to <a href="https://docs.sendgrid.com/ui/sending-email/sender-verification" ref="nofollow" target="_blank">verify the email address from which the emails will be sent</a>. This verification process involves adding the email address to your SendGrid account and proving that you have access to it. Failure to verify the email address can result in errors when attempting to send emails, such as authentication failures or email delivery issues. Therefore, it’s crucial to verify any email addresses you plan to use for sending emails to ensure reliable and error-free email delivery.</p>
<p>After these steps completed, you should have:</p>
<ol>
<li>The API key (a long abrakadabra string) like this: <code class="highlighter-rouge">SG.ve9OolaiLeenoh0iatoon0.hei8geiz1Shoorohtai6choopah8cahjae9vako8uBa</code></li>
<li>Verified domain, let it by <code class="highlighter-rouge">mydomain.com</code></li>
<li>Verified sender email address, let it be <code class="highlighter-rouge">hello@mydomain.com</code></li>
</ol>
<h3 id="configure-server">Configure server</h3>
<p>The Rails application will be configured via environment variables, which should be defined on your server (or on your local machine if you want to send emails from it):</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SMTP_DEFAULT_FROM: <your sender email>
SMTP_DOMAIN: <your domain>
SMTP_PASSWORD: <your api key>
SMTP_LOGIN: apikey
SMTP_PORT: 587
SMTP_SERVER: smtp.sendgrid.net
</code></pre></div></div>
<blockquote>
<p>Note that some of the values are static, while others are placeholders that you should replace with your own values. <code class="highlighter-rouge">SMTP_LOGIN</code>, <code class="highlighter-rouge">SMTP_PORT</code>, and <code class="highlighter-rouge">SMTP_SERVER</code> are static and will always have the same values. It is important to note that <code class="highlighter-rouge">SMTP_LOGIN</code> will always equal apikey, and you will not have your own value for that.</p>
</blockquote>
<p>Here is an example of how to do that using the Heroku command line:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>heroku config:set <span class="nv">SMTP_DEFAULT_FROM</span><span class="o">=</span><span class="s2">"Hello <hello@mydomain.com>"</span> <span class="nv">SMTP_DOMAIN</span><span class="o">=</span>mydomain.com <span class="nv">SMTP_LOGIN</span><span class="o">=</span>apikey <span class="nv">SMTP_PASSWORD</span><span class="o">=</span>SG.ve9OolaiLeenoh0iatoon0.hei8geiz1Shoorohtai6choopah8cahjae9vako8uBa <span class="nv">SMTP_PORT</span><span class="o">=</span>587
</code></pre></div></div>
<h3 id="configure-ruby-on-rails">Configure Ruby On Rails</h3>
<p>Emails are typically sent from a production server. Therefore, you need to add the necessary configuration to the <code class="highlighter-rouge">config/environments/production.rb</code> file as follows:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="n">config</span><span class="p">.</span><span class="nf">action_mailer</span><span class="p">.</span><span class="nf">smtp_settings</span> <span class="o">=</span> <span class="p">{</span>
<span class="ss">port: </span><span class="no">ENV</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s1">'SMTP_PORT'</span><span class="p">),</span>
<span class="ss">address: </span><span class="no">ENV</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s1">'SMTP_SERVER'</span><span class="p">),</span>
<span class="ss">user_name: </span><span class="no">ENV</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s1">'SMTP_LOGIN'</span><span class="p">),</span>
<span class="ss">password: </span><span class="no">ENV</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s1">'SMTP_PASSWORD'</span><span class="p">),</span>
<span class="ss">domain: </span><span class="no">ENV</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s1">'SMTP_DOMAIN'</span><span class="p">),</span>
<span class="ss">authentication: :plain</span><span class="p">,</span>
<span class="ss">enable_starttls_auto: </span><span class="kp">true</span>
<span class="p">}</span>
<span class="n">config</span><span class="p">.</span><span class="nf">action_mailer</span><span class="p">.</span><span class="nf">delivery_method</span> <span class="o">=</span> <span class="ss">:smtp</span>
</code></pre></div></div>
<p>Make sure that these settings are not overridden elsewhere in this file.</p>
<p>If you need to send emails from development or staging mode, add these lines to the respective files <code class="highlighter-rouge">config/environments/development.rb</code> or <code class="highlighter-rouge">config/environments/staging.rb</code>. Deliver these changes to your server.</p>
<h3 id="testing">Testing</h3>
<p>That’s all the steps needed to get it done. But before moving on to your next task, let’s make sure everything is working correctly. Open the Rails console and define a variable with the email address you want to send a verification email to (for this example, we’ll use <code class="highlighter-rouge">my@example.com</code>; replace it with your own):</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">to_email</span> <span class="o">=</span> <span class="s1">'my@example.com'</span>
</code></pre></div></div>
<p>And then paste there the following code snippet:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mailer</span> <span class="o">=</span> <span class="no">ActionMailer</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">new</span>
<span class="n">mailer</span><span class="p">.</span><span class="nf">mail</span><span class="p">(</span><span class="ss">from: </span><span class="no">ENV</span><span class="p">[</span><span class="s1">'SMTP_DEFAULT_FROM'</span><span class="p">],</span> <span class="ss">to: </span><span class="n">to_email</span><span class="p">,</span> <span class="ss">subject: </span><span class="s1">'Test from WideFix guide'</span><span class="p">,</span> <span class="ss">body: </span><span class="s2">"Hello, you've got mail!"</span><span class="p">).</span><span class="nf">deliver</span>
</code></pre></div></div>
<p>Wait a bit and you should receive the email in your inbox.</p>
<p>If you follow these steps correctly, all mailers defined in your Rails application will start using SendGrid for sending emails, and no additional configuration or checks are required.</p>
<p><a href="http://blog.widefix.com/send-emails-with-sendgrid-a-step-by-step-guide-for-ruby-on-rails-applications/">Send Emails with SendGrid: A Step-by-Step Guide for Ruby on Rails Applications</a> was originally published by Andrei Kaleshka at <a href="http://blog.widefix.com">Take your Rails development to the next level</a> on March 15, 2023.</p>http://blog.widefix.com/unlocking-the-power-of-chatgpt-with-ruby-a-beginners-guide2023-02-22 22:31:11 +0100T00:00:00-00:002023-02-22T00:00:00-05:00Andrei Kaleshkahttp://blog.widefix.comka8725@gmail.com
<p>Are you looking to harness the power of artificial intelligence and machine learning to streamline your coding workflow? ChatGPT and Ruby are two powerful tools that can help you do just that. However, if you’re new to these tools, getting started can be daunting. That’s where our beginner-friendly guide comes in. In this article, we’ll take you through everything you need to know to use ChatGPT with Ruby, from the basics of integration to more advanced features. Whether you’re a seasoned developer or just starting out, this guide will help you unlock the full potential of these powerful tools and take your coding skills to the next level.</p>
<p>Before you start integrating the ChatGPT API, you need to register for an API key and obtain your credentials from the ChatGPT website. To do that, follow this <a href="https://platform.openai.com/account/api-keys" ref="nofollow" target="_blank">page</a>.</p>
<p>Create the following class in your Ruby On Rails or any other Ruby-based project:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">OpenaiPrompt</span>
<span class="kp">extend</span> <span class="no">Dry</span><span class="o">::</span><span class="no">Initializer</span>
<span class="no">URL</span> <span class="o">=</span> <span class="s2">"https://api.openai.com/v1/completions"</span>
<span class="n">param</span> <span class="ss">:prompt</span>
<span class="n">option</span> <span class="ss">:model</span><span class="p">,</span> <span class="ss">default: </span><span class="nb">proc</span> <span class="p">{</span> <span class="s2">"text-davinci-003"</span> <span class="p">}</span>
<span class="n">option</span> <span class="ss">:max_tokens</span><span class="p">,</span> <span class="ss">default: </span><span class="nb">proc</span> <span class="p">{</span> <span class="mi">1000</span> <span class="p">}</span>
<span class="n">option</span> <span class="ss">:temperature</span><span class="p">,</span> <span class="ss">default: </span><span class="nb">proc</span> <span class="p">{</span> <span class="mi">0</span> <span class="p">}</span>
<span class="k">def</span> <span class="nf">call</span>
<span class="n">connection</span> <span class="o">=</span>
<span class="no">Faraday</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span> <span class="o">|</span><span class="n">faraday</span><span class="o">|</span>
<span class="n">faraday</span><span class="p">.</span><span class="nf">ssl</span><span class="p">[</span><span class="ss">:verify</span><span class="p">]</span> <span class="o">=</span> <span class="kp">false</span>
<span class="n">faraday</span><span class="p">.</span><span class="nf">headers</span> <span class="o">=</span> <span class="n">headers</span>
<span class="k">end</span>
<span class="n">response</span> <span class="o">=</span> <span class="n">connection</span><span class="p">.</span><span class="nf">post</span><span class="p">(</span><span class="no">URL</span><span class="p">,</span> <span class="n">body</span><span class="p">)</span>
<span class="n">json</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="nf">body</span><span class="p">)</span>
<span class="n">json</span><span class="p">[</span><span class="s2">"choices"</span><span class="p">].</span><span class="nf">first</span><span class="p">[</span><span class="s2">"text"</span><span class="p">]</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">body</span>
<span class="p">{</span>
<span class="ss">model: </span><span class="n">model</span><span class="p">,</span>
<span class="ss">prompt: </span><span class="n">prompt</span><span class="p">,</span>
<span class="ss">max_tokens: </span><span class="n">max_tokens</span><span class="p">,</span>
<span class="ss">temperature: </span><span class="n">temperature</span>
<span class="p">}.</span><span class="nf">to_json</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">headers</span>
<span class="p">{</span>
<span class="s2">"Content-Type"</span> <span class="o">=></span> <span class="s2">"application/json"</span><span class="p">,</span>
<span class="s2">"Authorization"</span> <span class="o">=></span> <span class="s2">"Bearer </span><span class="si">#{</span><span class="no">ENV</span><span class="p">[</span><span class="s1">'OPENAI_ACCESS_TOKEN'</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span><span class="p">,</span>
<span class="s2">"OpenAI-Organization"</span> <span class="o">=></span> <span class="no">ENV</span><span class="p">[</span><span class="s1">'OPENAI_ORGANIZATION_ID'</span><span class="p">]</span>
<span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>You can define your ChatGPT class within an existing file or create a new one specifically for it. However, it is generally recommended to create a separate file for your ChatGPT class for better organization and modularity of your code. This makes it easier to find and modify your ChatGPT code in the future without affecting other parts of your project. If it’s a Ruby On Rails application, <code class="highlighter-rouge">app/services/openai_prompt.rb</code> is a good file location for this piece of code.</p>
<p>This class has the following dependencies (gems) that should be added to your <code class="highlighter-rouge">Gemfile</code>:</p>
<ul>
<li><code class="highlighter-rouge">faraday</code> - This is a handy library that provides an abstract interface for making HTTP requests. It simplifies the process of making requests to external APIs and handling responses.</li>
<li><code class="highlighter-rouge">dry-initializer</code> - This library allows you to define the <code class="highlighter-rouge">new</code> method implicitly, reducing the amount of repetitive code required. This makes it easier to create new instances of your class without having to manually define instance variables and their default values.</li>
</ul>
<p>To use the <code class="highlighter-rouge">OpenaiPrompt</code> class, you first need to define the environment variable <code class="highlighter-rouge">OPENAI_ACCESS_TOKEN</code>. If you have multiple organizations registered within ChatGPT, you can also define the optional <code class="highlighter-rouge">OPENAI_ORGANIZATION_ID</code> environment variable.</p>
<p>Once you have set these environment variables, you can use the <code class="highlighter-rouge">OpenaiPrompt</code> class from any line of your project by simply calling <code class="highlighter-rouge">OpenaiPrompt.new('Why is Ruby an awesome programming language?').call</code>.</p>
<p>Here is an example of how you can use the <code class="highlighter-rouge">OpenaiPrompt</code> class in a Rails console:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">></span> <span class="no">OpenaiPrompt</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s1">'Why is Ruby awesome programming language?'</span><span class="p">).</span><span class="nf">call</span>
<span class="o">></span> <span class="c1"># => "\n\nRuby is an awesome programming language because it is easy to learn and use, has a large and supportive community, and is highly flexible and powerful. It is also object-oriented, meaning that it allows developers to create complex applications quickly and easily. Additionally, Ruby is open source, meaning that it is free to use and modify. Finally, Ruby is known for its readability, which makes it easier for developers to understand and debug code."</span>
</code></pre></div></div>
<p>ChatGPT offers a wide range of functionalities and models that can be utilized in various ways, not just limited to answering common questions. For instance, you can leverage GPT-3 to generate text for creative writing, content generation, or chatbot responses.</p>
<p>The <code class="highlighter-rouge">OpenaiPrompt</code> class serves as an example of how you can use ChatGPT to answer common questions programmatically. However, the same principles can be applied to build other classes that utilize ChatGPT’s other capabilities.</p>
<p>Furthermore, integrating ChatGPT with a user interface, such as a web form or a chatbot, can allow users to ask questions and get responses in a more intuitive manner. This can be particularly useful for applications that require natural language processing and understanding, such as customer service chatbots or virtual assistants. By integrating ChatGPT with a UI, you can make it more accessible and user-friendly for non-technical users.</p>
<p>If you have questions or need to integrate ChatGPT into your Ruby On Rails or any other Ruby-based project, please <a href="https://widefix.com/">contact</a>.</p>
<p><a href="http://blog.widefix.com/unlocking-the-power-of-chatgpt-with-ruby-a-beginners-guide/">Unlocking the Power of ChatGPT with Ruby</a> was originally published by Andrei Kaleshka at <a href="http://blog.widefix.com">Take your Rails development to the next level</a> on February 22, 2023.</p>http://blog.widefix.com/fake-time-dot-now-in-production2023-02-08 13:59:03 +0100T00:00:00-00:002023-02-08T00:00:00-05:00Andrei Kaleshkahttp://blog.widefix.comka8725@gmail.com
<p>TL;DR: see this <a href="https://gist.github.com/ka8725/95c21119b8fd4883925132ac0514f966" ref="nofollow" target="_blank">gist</a>.</p>
<p>As a Ruby on Rails expert, you may find yourself wondering how to debug a Rails application in production. In some cases, you may need to mimic the current time to check a time-dependent function result.</p>
<p>While there are third-party gems that offer the ability to fake <code class="highlighter-rouge">Time.now</code> functionality, Rails has this built-in functionality as part of the <code class="highlighter-rouge">ActiveSupport::Testing::TimeHelpers</code> module. However, this module is only intended for use in testing and not in production environments.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Time</span><span class="p">.</span><span class="nf">now</span> <span class="c1"># => Wed, 08 Feb 2023 14:01:33 UTC +00:00</span>
<span class="n">travel</span> <span class="mi">1</span><span class="p">.</span><span class="nf">day</span><span class="p">.</span><span class="nf">ago</span>
<span class="no">Time</span><span class="p">.</span><span class="nf">now</span> <span class="c1"># => Tue, 07 Feb 2023 14:01:33 UTC +00:00</span>
<span class="no">Date</span><span class="p">.</span><span class="nf">current</span> <span class="c1"># => Tue, 07 Feb 2023</span>
</code></pre></div></div>
<p>Check in production console:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">travel</span> <span class="mi">1</span><span class="p">.</span><span class="nf">day</span><span class="p">.</span><span class="nf">ago</span>
<span class="c1"># => Traceback (most recent call last):</span>
<span class="c1"># NoMethodError (undefined method `travel' for main:Object)</span>
</code></pre></div></div>
<p>If you want to use the <code class="highlighter-rouge">travel</code> or <code class="highlighter-rouge">freeze_time</code> method in production, you can load the module source code and include it into the console runtime:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mod</span> <span class="o">=</span> <span class="s2">"https://raw.githubusercontent.com/rails/rails/2a2a6ab6219b12e9e77931a60fe83c658db44ac7/activesupport/lib/active_support/testing/time_helpers.rb"</span>
<span class="nb">eval</span><span class="p">(</span><span class="nb">open</span><span class="p">(</span><span class="n">mod</span><span class="p">).</span><span class="nf">read</span><span class="p">)</span>
<span class="kp">include</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Testing</span><span class="o">::</span><span class="no">TimeHelpers</span>
<span class="n">travel_to</span><span class="p">(</span><span class="mi">1</span><span class="p">.</span><span class="nf">day</span><span class="p">.</span><span class="nf">ago</span><span class="p">)</span> <span class="p">{</span> <span class="no">Time</span><span class="p">.</span><span class="nf">now</span> <span class="p">}</span> <span class="c1"># => Tue, 07 Feb 2023 14:01:33 UTC +00:00</span>
</code></pre></div></div>
<blockquote>
<p>Tip: Use <code class="highlighter-rouge">Time.current</code> instead of <code class="highlighter-rouge">Time.now</code>. This blog post uses <code class="highlighter-rouge">Time.now</code> only for demonstration purposes due to its widespread usage over <code class="highlighter-rouge">Time.current</code>.</p>
</blockquote>
<p>Here’s an example of how I recently implemented this in a production environment. In our project, we have a function called <code class="highlighter-rouge">Plan#stripe_id</code> that links to a product pricing object. The pricing is dependent on the current time because on February 15th, the project’s prices will be increased. Before February 15th, the system uses the old prices, but after that date, it uses the new prices.</p>
<p>Here’s how the method is defined:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Plan</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="no">NEW_PRICES_TIME</span> <span class="o">=</span> <span class="s1">'15 Feb 21:00 UTC'</span><span class="p">.</span><span class="nf">to_datetime</span>
<span class="k">def</span> <span class="nf">stripe_id</span>
<span class="k">if</span> <span class="no">NEW_PRICES_TIME</span><span class="p">.</span><span class="nf">past?</span>
<span class="n">stripe_price_id</span> <span class="o">||</span> <span class="nb">name</span>
<span class="k">else</span>
<span class="nb">name</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Please note that name is a deprecated column referring to an outdated Stripe product price.</p>
<p>Once this feature is deployed to production, we will verify its functionality across all plans by accessing the production Rails console and reviewing the pre-implementation values.</p>
<p>Checking how it works now:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Plan</span><span class="p">.</span><span class="nf">all</span><span class="p">.</span><span class="nf">map</span><span class="p">(</span><span class="o">&</span><span class="ss">:name</span><span class="p">)</span> <span class="c1"># => ["a", "b"]</span>
</code></pre></div></div>
<p>And then we do the same at the traveled time after Feb 15:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mod</span> <span class="o">=</span> <span class="s2">"https://raw.githubusercontent.com/rails/rails/2a2a6ab6219b12e9e77931a60fe83c658db44ac7/activesupport/lib/active_support/testing/time_helpers.rb"</span>
<span class="nb">eval</span><span class="p">(</span><span class="nb">open</span><span class="p">(</span><span class="n">mod</span><span class="p">).</span><span class="nf">read</span><span class="p">)</span>
<span class="kp">include</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Testing</span><span class="o">::</span><span class="no">TimeHelpers</span>
<span class="n">travel_to</span><span class="p">(</span><span class="s2">"16 Feb, 2024"</span><span class="p">.</span><span class="nf">to_datetime</span><span class="p">)</span> <span class="p">{</span> <span class="no">Plan</span><span class="p">.</span><span class="nf">all</span><span class="p">.</span><span class="nf">map</span><span class="p">(</span><span class="o">&</span><span class="ss">:name</span><span class="p">)</span> <span class="p">}</span> <span class="c1"># => ["a_2023", "b_2023"]</span>
</code></pre></div></div>
<p>With this verification process, we can now confidently say that the production environment is functioning correctly and will be utilizing the correct prices after Feb 15, thereby avoiding any unexpected issues in the final stages.</p>
<h3 id="be-aware-of-the-following-security-concerns-before-proceeding-with-these-steps">Be aware of the following security concerns before proceeding with these steps</h3>
<p>Loading code from an external source via an HTTP link can pose a security risk as it may contain malicious code. A recommended solution is to use unit tests. However, unit tests cannot guarantee the behavior of code in a production environment. In emergency situations where time is of the essence, a faster and more creative approach may be necessary to ensure the code’s integrity.</p>
<p>The security concern can be addressed by creating the link yourself. To do so, visit the GitHub Rails repository, search for the <code class="highlighter-rouge">time_helpers.rb</code> file, and click the “Raw” button. This will generate the link. However, it is important to note that even this manual process does not guarantee the absence of malicious code, as the resource may have been tampered with by hackers. Always review the contents before evaluating them:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">url</span> <span class="o">=</span> <span class="s2">"<HTTP LINK>"</span>
<span class="n">code</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="n">url</span><span class="p">).</span><span class="nf">read</span>
<span class="nb">puts</span> <span class="n">code</span>
</code></pre></div></div>
<p>It’s important to carefully review the code before evaluating it, to ensure it is the original, intended code and not something malicious. Only after confirming the authenticity of the code should you include it as a module:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">eval</span><span class="p">(</span><span class="n">code</span><span class="p">)</span>
<span class="kp">include</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Testing</span><span class="o">::</span><span class="no">TimeHelpers</span>
</code></pre></div></div>
<p>If there is limited time to review the code, a simple check can help prevent dangerous actions. This can be done by raising an error if a specific line of code is not present. This will serve as a safeguard and ensure that any malicious code is not executed:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">raise</span> <span class="s1">'dangerous code'</span> <span class="k">unless</span> <span class="n">code</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="s1">'module ActiveSupport'</span><span class="p">)</span>
</code></pre></div></div>
<p>Manipulating production systems can be risky and should only be done with a high level of confidence in what you are doing. Although in some scenarios, the console is the only solution to fix a production issue, it should be approached with caution. Before making any changes, consider the potential impact and weigh the decision carefully, as even a small mistake can have significant consequences. In some cases, the cost of fixing the issue through other means may be less than the cost of fixing the mistake made in the production console.</p>
<p>At <a href="https://widefix.com" ref="nofollow" target="_blank">WideFix</a>, we take pride in our expertise and experience. You can trust that our solutions will not harm your business and will always keep your site running smoothly. You can rely on us to provide confident and effective solutions that meet your needs.</p>
<div style="display: flex;align-items:center;justify-content: center;margin-top: 20px;">
<a class="btn" style="background-color: #f04338; cursor: pointer;font-size: 24px;" target="_blank" rel="nofollow" href="https://widefix.com">Hire Ruby On Rails expert now</a>
</div>
<p><a href="http://blog.widefix.com/fake-time-dot-now-in-production/">Fake Time.now in production</a> was originally published by Andrei Kaleshka at <a href="http://blog.widefix.com">Take your Rails development to the next level</a> on February 08, 2023.</p>http://blog.widefix.com/ask-google-to-recrawl-your-urls2023-01-31 10:01:28 +0100T00:00:00-00:002023-01-31T00:00:00-05:00Andrei Kaleshkahttp://blog.widefix.comka8725@gmail.com
<p>A commonly suggested recommendation from many SEO specialists is as follows:</p>
<blockquote>
<p>You can request Google to recrawl your website by following these steps:</p>
<ol>
<li>Verify your website ownership in Google Search Console</li>
<li>Submit your sitemap to Google through the Search Console</li>
<li>Fetch as Google feature in the Search Console to request indexing of individual pages</li>
<li>Use the “Submit URL” feature in Google Search Console for new or updated pages</li>
<li>Monitor your website’s performance in the Search Console to track indexing status.</li>
</ol>
<p>Note: Recrawling may take several days or weeks, and it is not guaranteed to happen immediately.</p>
</blockquote>
<p>All of that is correct. Unfortunately, in practice, one might encounter the following scenario:</p>
<ul>
<li>A page was indexed by Google a long time ago.</li>
<li>The sitemap was submitted recently.</li>
<li>Submitting all the pages one by one would take too much time, so it was not done.</li>
<li>Several weeks have passed, but the page has still not been reindexed.</li>
</ul>
<p>If you face this issue, the automated option is available. This article includes an example of how <strong>to request Google to reindex a group of URLs</strong> using the <strong>Ruby programming language</strong>.</p>
<p>First, register your project in the Google API Console and obtain a JSON private key file. Follo <a href="https://developers.google.com/search/apis/indexing-api/v3/prereqs" ref="nofollow" target="_blank">these instructions</a> on how to do so.</p>
<p>Next, with the preparation step complete, we move on to the scripting. Let’s say we have a list of artists and songs that need to be reindexed. The following Ruby script will schedule their reindex in a bulk request, meaning it will make only one HTTP request instead of multiple. The script implies <a href="https://github.com/googleapis/google-api-ruby-client" ref="nofollow" target="_blank">google-api-ruby-client gem</a> installed.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'google/apis/indexing_v3'</span>
<span class="nb">require</span> <span class="s1">'googleauth'</span>
<span class="n">api</span> <span class="o">=</span> <span class="no">Google</span><span class="o">::</span><span class="no">Apis</span><span class="o">::</span><span class="no">IndexingV3</span><span class="o">::</span><span class="no">IndexingService</span><span class="p">.</span><span class="nf">new</span>
<span class="c1"># uncomment the line below to see more details</span>
<span class="c1"># Google::Apis.logger.level = Logger::DEBUG</span>
<span class="n">api</span><span class="p">.</span><span class="nf">authorization</span> <span class="o">=</span>
<span class="no">Google</span><span class="o">::</span><span class="no">Auth</span><span class="o">::</span><span class="no">ServiceAccountCredentials</span><span class="p">.</span><span class="nf">make_creds</span><span class="p">(</span>
<span class="ss">json_key_io: </span><span class="no">File</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="no">Rails</span><span class="p">.</span><span class="nf">root</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s1">'config/google-api-creds.json'</span><span class="p">)),</span>
<span class="ss">scope: </span><span class="s1">'https://www.googleapis.com/auth/indexing'</span>
<span class="p">)</span>
<span class="n">api</span><span class="p">.</span><span class="nf">authorization</span><span class="p">.</span><span class="nf">fetch_access_token!</span>
<span class="c1"># all published songs and all artists will be scheduled to reindex by Google</span>
<span class="n">api</span><span class="p">.</span><span class="nf">batch</span> <span class="k">do</span> <span class="o">|</span><span class="n">batch</span><span class="o">|</span>
<span class="no">Song</span><span class="p">.</span><span class="nf">published</span><span class="p">.</span><span class="nf">find_each</span> <span class="k">do</span> <span class="o">|</span><span class="n">song</span><span class="o">|</span>
<span class="n">url</span> <span class="o">=</span>
<span class="no">Google</span><span class="o">::</span><span class="no">Apis</span><span class="o">::</span><span class="no">IndexingV3</span><span class="o">::</span><span class="no">UrlNotification</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span>
<span class="ss">url: </span><span class="s2">"https://mysite.com/songs/</span><span class="si">#{</span><span class="n">song</span><span class="p">.</span><span class="nf">id</span><span class="si">}</span><span class="s2">"</span><span class="p">,</span>
<span class="ss">type: </span><span class="s1">'URL_UPDATED'</span>
<span class="p">)</span>
<span class="n">batch</span><span class="p">.</span><span class="nf">publish_url_notification</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
<span class="k">end</span>
<span class="no">Artist</span><span class="p">.</span><span class="nf">find_each</span> <span class="k">do</span> <span class="o">|</span><span class="n">artist</span><span class="o">|</span>
<span class="n">url</span> <span class="o">=</span>
<span class="no">Google</span><span class="o">::</span><span class="no">Apis</span><span class="o">::</span><span class="no">IndexingV3</span><span class="o">::</span><span class="no">UrlNotification</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span>
<span class="ss">url: </span><span class="s2">"https://mysite.com/artists/</span><span class="si">#{</span><span class="n">artist</span><span class="p">.</span><span class="nf">id</span><span class="si">}</span><span class="s2">"</span><span class="p">,</span>
<span class="ss">type: </span><span class="s1">'URL_UPDATED'</span>
<span class="p">)</span>
<span class="n">batch</span><span class="p">.</span><span class="nf">publish_url_notification</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Assuming the script is saved in <code class="highlighter-rouge">scripts/google.rb</code> in a typical Rails application and the private key JSON file is located in <code class="highlighter-rouge">config/google-api-creds.json</code>, it can be run with the following command:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails runner scripts/google.rb
</code></pre></div></div>
<p>This will reschedule all links at once. However, keep in mind that Google has a default quota of 200 requests per day, which can be increased if necessary. More information can be found <a href="https://developers.google.com/search/apis/indexing-api/v3/quota-pricing" ref="nofollow" target="_blank">here</a>.</p>
<p>Additionally, we can verify that the links were requested to be updated using a script. The following code will go through all entities and check when they were requested for recrawling by Google. The results will be saved in the <code class="highlighter-rouge">urls.csv</code> file.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'google/apis/indexing_v3'</span>
<span class="nb">require</span> <span class="s1">'googleauth'</span>
<span class="n">api</span> <span class="o">=</span> <span class="no">Google</span><span class="o">::</span><span class="no">Apis</span><span class="o">::</span><span class="no">IndexingV3</span><span class="o">::</span><span class="no">IndexingService</span><span class="p">.</span><span class="nf">new</span>
<span class="c1"># uncomment the line below to see more details</span>
<span class="c1"># Google::Apis.logger.level = Logger::DEBUG</span>
<span class="n">api</span><span class="p">.</span><span class="nf">authorization</span> <span class="o">=</span>
<span class="no">Google</span><span class="o">::</span><span class="no">Auth</span><span class="o">::</span><span class="no">ServiceAccountCredentials</span><span class="p">.</span><span class="nf">make_creds</span><span class="p">(</span>
<span class="ss">json_key_io: </span><span class="no">File</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="no">Rails</span><span class="p">.</span><span class="nf">root</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s1">'config/google-api-creds.json'</span><span class="p">)),</span>
<span class="ss">scope: </span><span class="s1">'https://www.googleapis.com/auth/indexing'</span>
<span class="p">)</span>
<span class="n">api</span><span class="p">.</span><span class="nf">authorization</span><span class="p">.</span><span class="nf">fetch_access_token!</span>
<span class="nb">require</span> <span class="s1">'csv'</span>
<span class="no">CSV</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="s1">'urls.csv'</span><span class="p">,</span> <span class="s1">'wb'</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">csv</span><span class="o">|</span>
<span class="no">Song</span><span class="p">.</span><span class="nf">published</span><span class="p">.</span><span class="nf">find_each</span> <span class="k">do</span> <span class="o">|</span><span class="n">song</span><span class="o">|</span>
<span class="n">url</span> <span class="o">=</span> <span class="s2">"https://mysite.com/songs/</span><span class="si">#{</span><span class="n">song</span><span class="p">.</span><span class="nf">id</span><span class="si">}</span><span class="s2">"</span>
<span class="n">res</span> <span class="o">=</span> <span class="n">api</span><span class="p">.</span><span class="nf">get_url_notification_metadata</span><span class="p">(</span><span class="ss">url: </span><span class="n">url</span><span class="p">)</span>
<span class="n">csv</span> <span class="o"><<</span> <span class="p">[</span><span class="n">song</span><span class="p">.</span><span class="nf">id</span><span class="p">,</span> <span class="n">res</span><span class="p">.</span><span class="nf">url</span><span class="p">,</span> <span class="n">res</span><span class="p">.</span><span class="nf">latest_update</span><span class="o">&</span><span class="p">.</span><span class="nf">notify_time</span><span class="p">]</span>
<span class="k">end</span>
<span class="no">Artist</span><span class="p">.</span><span class="nf">find_each</span> <span class="k">do</span> <span class="o">|</span><span class="n">artist</span><span class="o">|</span>
<span class="n">url</span> <span class="o">=</span> <span class="s2">"https://mysite.com/artists/</span><span class="si">#{</span><span class="n">artist</span><span class="p">.</span><span class="nf">id</span><span class="si">}</span><span class="s2">"</span>
<span class="n">res</span> <span class="o">=</span> <span class="n">api</span><span class="p">.</span><span class="nf">get_url_notification_metadata</span><span class="p">(</span><span class="ss">url: </span><span class="n">url</span><span class="p">)</span>
<span class="n">csv</span> <span class="o"><<</span> <span class="p">[</span><span class="n">artist</span><span class="p">.</span><span class="nf">id</span><span class="p">,</span> <span class="n">res</span><span class="p">.</span><span class="nf">url</span><span class="p">,</span> <span class="n">res</span><span class="p">.</span><span class="nf">latest_update</span><span class="o">&</span><span class="p">.</span><span class="nf">notify_time</span><span class="p">]</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>By scheduling your pages for Google recrawl in this manner, the pages will appear in Google much faster and you won’t have to wait for weeks or even months.</p>
<p>For more information and details on how it works, see <a href="https://developers.google.com/search/apis/indexing-api/v3/quickstart" ref="nofollow" target="_blank">here</a>. Also, note that there are other programming languages available for this type of scripting. A list of available libraries for other programming lagunages can be found on the <a href="https://developers.google.com/search/apis/indexing-api/v3/libraries" ref="nofollow" target="_blank">official page</a>.</p>
<p>If you have questions or need a script like this for your project, feel free to <a href="https://widefix.com/">contacting us</a>.</p>
<p><a href="http://blog.widefix.com/ask-google-to-recrawl-your-urls/">Ask Google to recrawl your URLs</a> was originally published by Andrei Kaleshka at <a href="http://blog.widefix.com">Take your Rails development to the next level</a> on January 31, 2023.</p>http://blog.widefix.com/widefix-wins-award-for-best-logo-of-20232023-01-26 17:15:36 +0100T00:00:00-00:002023-01-26T00:00:00-05:00Andrei Kaleshkahttp://blog.widefix.comka8725@gmail.com
<p>WideFix, a company known for its expertise in maintaining and optimizing Ruby On Rails applications, has been <a href="https://www.designrush.com/best-designs/logo">recognized as having one of the best logos of 2023</a>. The company’s logo, which features a blue bird holding an orange envelope, represents the company’s commitment to delivering good news and efficient solutions to its clients.</p>
<p>The design of the logo was the result of a collaboration between WideFix’s marketing team and a leading branding agency. The team worked closely together to create a logo that accurately represents the company’s values and mission, while also being visually striking and memorable.</p>
<p>The color scheme used in the logo, blue and orange, is meant to convey a sense of trust, efficiency and innovation. The bird symbolize the message of good news that the company bring to their clients.</p>
<p>WideFix’s logo has received widespread praise for its simplicity and elegance. It effectively communicates the company’s mission to provide comprehensive and innovative solutions to its clients, and its design is both timeless and modern.</p>
<p>The company’s commitment to providing top-notch services is reflected in the design of its logo, and it is no surprise that it has been recognized as one of the best logos of the year. WideFix continues to be a leader in the industry, and it is expected that the company and its iconic logo will continue to be synonymous with excellence and innovation in the years to come.</p>
<p>In conclusion, WideFix’s logo is more than just a graphic representation of the company, it’s a visual representation of their mission, values and commitment to providing the best service to their clients. This recognition as one of the best logos of 2023 is a testament to the hard work and dedication of the company and its team.</p>
<p>–</p>
<p>At WideFix, we are experts in Ruby on Rails and have the experience and knowledge to create and maintain high-quality web applications. If you are looking to build a custom web-based application, an e-commerce platform, a content management system, or a customer relationship management system, we can help you achieve your goals quickly and efficiently. Our team of experts has a deep understanding of the Ruby language and the Rails framework, and we are ready to put our skills to work for you. Don’t hesitate, contact us today to discuss your project and see how we can help you take your business to the next level with the power of Ruby on Rails. Visit our <a href="https://widefix.com/">official page</a> for more details.</p>
<p><a href="http://blog.widefix.com/widefix-wins-award-for-best-logo-of-2023/">WideFix wins award for Best Logo of 2023</a> was originally published by Andrei Kaleshka at <a href="http://blog.widefix.com">Take your Rails development to the next level</a> on January 26, 2023.</p>http://blog.widefix.com/what-is-ruby-on-rails-good-for2023-01-22 22:53:09 +0100T00:00:00-00:002023-01-22T00:00:00-05:00Andrei Kaleshkahttp://blog.widefix.comka8725@gmail.com
<p>Ruby on Rails, also known as Rails, is a web application framework that is primarily used for building web applications. It is written in the Ruby programming language and is designed to make programming web applications easier by making assumptions about what developers need to get started.</p>
<p>Rails is good for a variety of web development tasks, some of which include:</p>
<ul>
<li><strong>Building custom web-based applications</strong>: Rails can be used to build a wide variety of custom web-based applications, such as inventory management systems, accounting systems, and content management systems.</li>
<li><strong>E-commerce</strong>: Rails can be used to develop e-commerce platforms to sell products or services online.</li>
<li><strong>Content management systems</strong>: Rails can be used to create content management systems that allow users to easily update and manage a company’s website.</li>
<li><strong>Customer Relationship Management (CRM) systems</strong>: Rails can be used to build CRM systems to manage interactions with clients and customers.</li>
<li><strong>API development</strong>: Rails can be used to build <strong>RESTful</strong> (or <strong>GraphQL</strong>) APIs for integrating with other systems and services.</li>
</ul>
<p>Rails is also known for its ability to quickly and easily scale as a business grows. Additionally, the Ruby language and Rails framework have a large, active community, meaning that there is a wealth of resources and support available to developers.</p>
<p>In summary, Ruby on Rails is a good choice for web application development, and it is particularly well-suited for building custom web-based applications, e-commerce platforms, content management systems, CRM systems and API development. Additionally, the framework’s ability to scale and the large community of developers make it an attractive choice for businesses of all sizes.</p>
<p>At WideFix, we are experts in Ruby on Rails and have the experience and knowledge to create and maintain high-quality web applications. If you are looking to build a custom web-based application, an e-commerce platform, a content management system, or a customer relationship management system, we can help you achieve your goals quickly and efficiently. Our team of experts has a deep understanding of the Ruby language and the Rails framework, and we are ready to put our skills to work for you. Don’t hesitate, contact us today to discuss your project and see how we can help you take your business to the next level with the power of Ruby on Rails. Visit our <a href="https://widefix.com/">official page</a> for more details.</p>
<p><a href="http://blog.widefix.com/what-is-ruby-on-rails-good-for/">What is Ruby on Rails good for</a> was originally published by Andrei Kaleshka at <a href="http://blog.widefix.com">Take your Rails development to the next level</a> on January 22, 2023.</p>http://blog.widefix.com/hire-expert-ruby-on-rails-developer2023-01-07 15:00:00 +0000T00:00:00-00:002023-01-17T00:00:00-05:00Andrei Kaleshkahttp://blog.widefix.comka8725@gmail.com
<p>Hiring an expert Ruby on Rails developer can be a crucial step for your business success. Ruby on Rails, also known as Rails, is a popular web application framework written in the Ruby programming language. It is known for its simplicity and ease of use, making it a favorite among developers.</p>
<p>When looking to hire an expert Ruby on Rails developer, there are a few key factors to consider. Firstly, you want to make sure that the developer has a strong understanding of the Ruby programming language as well as the Rails framework. This will ensure that they can effectively build and maintain your web application.</p>
<p>Secondly, you want to look for a developer who has experience working on projects similar to yours. This will give you a good idea of their capabilities and how they can apply their skills to your project.</p>
<p>Thirdly, it is important to consider the developer’s communication skills. A good developer should be able to effectively communicate with your team and understand your project’s requirements. This will help ensure that your project stays on track and is completed within the desired timeframe.</p>
<p>Lastly, it is important to ensure that the developer has a good understanding of the latest web development trends and technologies. This will help ensure that your web application is built using the latest and most efficient technologies available.</p>
<p>In conclusion, hiring an expert Ruby on Rails developer is a great way to ensure that your web application is built to the highest standards. By considering the factors discussed above, you can find the right developer for your project and take your business to the next level.</p>
<p><a href="http://blog.widefix.com/hire-expert-ruby-on-rails-developer/">Hire expert Ruby On Rails developer</a> was originally published by Andrei Kaleshka at <a href="http://blog.widefix.com">Take your Rails development to the next level</a> on January 17, 2023.</p>http://blog.widefix.com/spike-of-signups-business-threat2023-01-04 15:00:00 +0000T00:00:00-00:002023-01-04T00:00:00-05:00Andrei Kaleshkahttp://blog.widefix.comka8725@gmail.com
<p><strong>TL;DR:</strong> Use captcha and email verification services.</p>
<p>Recently, we experienced a spike in the number of signed up customers within a <strong>Ruby on Rails application</strong> that we provide maintenance services for. As shown in the following graph, the spike occurred without any aggressive marketing campaigns:</p>
<p><img src="/images/stripe_spike.jpg" alt="Signups spike in Stripe" /></p>
<p>While an increase in new customers is generally a good thing, this particular spike turned out to be a false positive. It was likely the result of an attack, as almost all of the new signups used fake emails and the credit cards used for the signups were passed by Stripe without a CVC check.</p>
<p>To resolve this issue, we implemented a few solutions:</p>
<ul>
<li>Email verification: We added an email verification step to the signup process using the Mandrill API. This was an easy solution as the project was already using Mandrill to send emails.</li>
<li>reCaptcha: We added a reCaptcha to the signup form to help prevent fake signups.</li>
<li>Data cleanup: We removed the fake accounts from Drip, Stripe, and our database to clean up the data.</li>
</ul>
<p>As a result, the number of new signups returned to a normal level, as shown in the following graph:</p>
<p><img src="/images/normal_signups.jpg" alt="Normal signups in Stripe" /></p>
<p>Overall, implementing email verification and reCaptcha helped to stabilize the number of new signups and protect the business and customer data. It’s important to take necessary precautions to prevent fake signups and other potential attacks on your application. In this case, quick action helped to mitigate the impact of the fake signups and keep the business running smoothly.</p>
<p>If you have a <strong>Ruby on Rails application</strong> that needs maintenance, you’re in the right place. At <a href="https://clutch.co/profile/widefix">WideFix</a>, we specialize in providing comprehensive maintenance services for Ruby on Rails applications. Our team of experienced developers is well-versed in all aspects of Ruby on Rails, so you can trust us to keep your application running smoothly.</p>
<p>Whether you need help fixing bugs, adding new features, or simply keeping your application up-to-date with the latest security patches, we’ve got you covered. Our maintenance services include:</p>
<ul>
<li>
<p><strong>Bug fixing</strong>: If you’ve encountered any errors or issues with your application, we can quickly identify and fix the problem to ensure that your application is running smoothly.</p>
</li>
<li>
<p><strong>Feature additions</strong>: If you want to add new functionality to your application, we can help you plan and implement the changes you need.</p>
</li>
<li>
<p><strong>Security updates</strong>: Keeping your application secure is important, and we can help you ensure that your application is up-to-date with the latest security patches and best practices.</p>
</li>
<li>
<p><strong>Performance optimization</strong>: If your application is running slowly or experiencing other performance issues, we can help you identify and resolve the problem to improve the speed and reliability of your application.</p>
</li>
</ul>
<p>In addition to these services, we also offer ongoing maintenance plans to ensure that your application stays in top shape. With our maintenance plan, you’ll receive regular updates and support to keep your application running smoothly.</p>
<p>If you’re interested in learning more about our maintenance services for Ruby on Rails applications, don’t hesitate to <a href="mailto:call@widefix.com">contact us</a>. Our team would be happy to discuss your needs and provide you with a customized quote.</p>
<p><a href="http://blog.widefix.com/spike-of-signups-business-threat/">Fake signups: a threat to your business</a> was originally published by Andrei Kaleshka at <a href="http://blog.widefix.com">Take your Rails development to the next level</a> on January 04, 2023.</p>