<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Cies Breijs</title>
    <description>The latest articles on DEV Community by Cies Breijs (@cies).</description>
    <link>https://dev.to/cies</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2782723%2F91505ad3-86ef-4267-a3e6-aa5b581ccd64.jpeg</url>
      <title>DEV Community: Cies Breijs</title>
      <link>https://dev.to/cies</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cies"/>
    <language>en</language>
    <item>
      <title>Postgres pipelines from the JVM with Bpdbi</title>
      <dc:creator>Cies Breijs</dc:creator>
      <pubDate>Tue, 24 Mar 2026 17:00:31 +0000</pubDate>
      <link>https://dev.to/cies/postgres-pipelines-from-the-jvm-with-bpdbi-14cd</link>
      <guid>https://dev.to/cies/postgres-pipelines-from-the-jvm-with-bpdbi-14cd</guid>
      <description>&lt;p&gt;Every time your JVM app talks to Postgres over &lt;a href="https://en.wikipedia.org/wiki/Java_Database_Connectivity" rel="noopener noreferrer"&gt;JDBC&lt;/a&gt;, something wasteful happens. Your code sends a query, waits for the response, sends the next query, waits again, etc. Each wait is a full network round-trip with a typical latency of 0.5-2ms in a cloud environment. For a simple transaction with a few queries, that's 8-10ms of just... waiting.&lt;/p&gt;

&lt;p&gt;This problem is exacerbated by using db transactions and the additional queries that are commonly added when using Postgres' Row-Level Security (RLS). For instance when using &lt;a href="https://supabase.com" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt;, a role and JWT claims need to be set before executing the actual query. Usually this is done more than once for each HTTP request, as some db queries need to be executed with a different role than others.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/bpdbi/bpdbi" rel="noopener noreferrer"&gt;Bpdbi&lt;/a&gt; is a new JVM database library for the Postgres database. It exposes &lt;strong&gt;pipelining&lt;/strong&gt;, a feature that's been part of the Postgres wire protocol since version 14.&lt;/p&gt;

&lt;p&gt;JDBC cannot do pipelines. &lt;em&gt;Bpdbi can&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Where this came from
&lt;/h2&gt;

&lt;p&gt;Bpdbi started as a port of the &lt;a href="https://github.com/eclipse-vertx/vertx-sql-client" rel="noopener noreferrer"&gt;Vert.x SQL Client&lt;/a&gt; — the database layer behind Quarkus and one of the fastest JVM database drivers. Vert.x already speaks the Postgres wire protocol directly (no JDBC), and exposes the pipelines feature.&lt;/p&gt;

&lt;p&gt;Vert.x is fully reactive (async/non-blocking). Reactive code is not for everyone: it adds significant complexity &lt;a href="https://rajendravardi.medium.com/the-hidden-dangers-of-reactive-programming-what-every-developer-must-know-9ce467552fd3" rel="noopener noreferrer"&gt;¹&lt;/a&gt; &lt;a href="https://www.techyourchance.com/reactive-programming-considered-harmful" rel="noopener noreferrer"&gt;²&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Reactive code looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Reactive — same logic, now good luck&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Uni&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Invoice&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;generateInvoice&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;orderRepo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;flatMap&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
          &lt;span class="nc"&gt;Uni&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;combine&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
              &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;unis&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                  &lt;span class="n"&gt;customerRepo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCustomerId&lt;/span&gt;&lt;span class="o"&gt;()),&lt;/span&gt;
                  &lt;span class="n"&gt;itemRepo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByOrderId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
              &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;asTuple&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
              &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tuple&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;invoiceService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                  &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tuple&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getItem1&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;tuple&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getItem2&lt;/span&gt;&lt;span class="o"&gt;())));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While the blocking equivalent look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Invoice&lt;/span&gt; &lt;span class="nf"&gt;generateInvoice&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderRepo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="nc"&gt;Customer&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;customerRepo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCustomerId&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
  &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Item&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;itemRepo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByOrderId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;invoiceService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reactive allows for higher throughput in high-traffic scenarios, but comes at a cost: less readable code, useless stack traces, and a paradigm that infects your code base. With Java 21's virtual threads, blocking I/O became much cheaper — you get thousands of concurrent connections without platform threads.&lt;/p&gt;

&lt;p&gt;Bpdbi started as a port of &lt;em&gt;Vert.x SQL Client&lt;/em&gt;, stripped of all its async/reactive machinery. Where Vert.x SQL uses &lt;a href="https://netty.io" rel="noopener noreferrer"&gt;Netty&lt;/a&gt; to connect with the database, Bpdbi uses a good old &lt;code&gt;java.net.Socket&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Bpdbi employs Postgres' binary protocol and pipelines for &lt;strong&gt;all&lt;/strong&gt; db queries, even single queries without parameters. This results in a simpler library with a much smaller footprint, while being very performant (as shown in the benchmarks).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Bpdbi provides blocking, pipelined, small-footprint and performant Postgres access for the JVM.&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Pipelining in practice
&lt;/h2&gt;

&lt;p&gt;Here's the core idea. Say you need to start a transaction, set some config, and run a query. With JDBC, that's four separate round-trips:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createStatement&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"BEGIN"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createStatement&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SET statement_timeout TO '5s'"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createStatement&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SET LOCAL role TO 'authenticated'"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;PreparedStatement&lt;/span&gt; &lt;span class="n"&gt;ps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prepareStatement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT * FROM orders WHERE id = ?"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;ps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setInt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;ResultSet&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;executeQuery&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Bpdbi, it's one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enqueue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"BEGIN"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enqueue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SET statement_timeout TO '5s'"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enqueue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SET LOCAL role TO 'authenticated'"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;RowSet&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT * FROM orders WHERE id = $1"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;enqueue()&lt;/code&gt; buffers statements locally. &lt;code&gt;query()&lt;/code&gt; flushes everything —all the enqueued statements plus itself— in a single TCP write. The Postgres instance processes them all and sends all responses back at once. This feature is called pipelining.&lt;/p&gt;

&lt;h3&gt;
  
  
  When you need all the results
&lt;/h3&gt;

&lt;p&gt;Sometimes you want results from multiple pipelined queries. &lt;code&gt;flush()&lt;/code&gt; returns them all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enqueue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"BEGIN"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;aliceQx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enqueue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INSERT INTO users (name) VALUES (:name) RETURNING id"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Alice"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;bobQx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enqueue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INSERT INTO users (name) VALUES (:name) RETURNING id"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Bob"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enqueue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"COMMIT"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RowSet&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;flush&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;aliceId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aliceQx&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;first&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getLong&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;bobId&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bobQx&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;first&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getLong&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four statements, one round-trip. Each &lt;code&gt;enqueue()&lt;/code&gt; returns an index so you know which result is which.&lt;/p&gt;

&lt;h3&gt;
  
  
  The benchmark numbers
&lt;/h3&gt;

&lt;p&gt;We ran JMH benchmarks using Toxiproxy to simulate 1ms network latency per direction (2ms round-trip). This simulates what you'd see talking to a database in the same cloud region.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Bpdbi&lt;/th&gt;
&lt;th&gt;JDBC (&lt;code&gt;pgjdbc&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10 SELECTs (pipelined vs sequential)&lt;/td&gt;
&lt;td&gt;310 ops/s&lt;/td&gt;
&lt;td&gt;18 ops/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;17x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transaction (BEGIN+SELECT+COMMIT)&lt;/td&gt;
&lt;td&gt;360 ops/s&lt;/td&gt;
&lt;td&gt;185 ops/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~2x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 INSERTs in a transaction&lt;/td&gt;
&lt;td&gt;116 ops/s&lt;/td&gt;
&lt;td&gt;18 ops/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6.5x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor fetch (1000 rows)&lt;/td&gt;
&lt;td&gt;281 ops/s&lt;/td&gt;
&lt;td&gt;30 ops/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9.3x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bulk insert (100 rows)&lt;/td&gt;
&lt;td&gt;313 ops/s&lt;/td&gt;
&lt;td&gt;171 ops/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.8x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single row lookup&lt;/td&gt;
&lt;td&gt;370 ops/s&lt;/td&gt;
&lt;td&gt;370 ops/s&lt;/td&gt;
&lt;td&gt;op par&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-row fetch (10 rows)&lt;/td&gt;
&lt;td&gt;358 ops/s&lt;/td&gt;
&lt;td&gt;358 ops/s&lt;/td&gt;
&lt;td&gt;on par&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern is clear: anything that touches the network more than once gets a massive speedup. While single-query performance is on par with JDBC + &lt;code&gt;pgjdbc&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Postgres-only, and that's the point
&lt;/h2&gt;

&lt;p&gt;Bpdbi only supports Postgres, the only open source database that truly supports pipelining. This is intentional, and it buys us a lot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Binary protocol everywhere
&lt;/h3&gt;

&lt;p&gt;The Postgres extended query protocol lets you request results in binary format. An integer comes back as four raw bytes instead of the text string &lt;code&gt;"12345"&lt;/code&gt;. A UUID is 16 bytes instead of 36 characters. No string allocation and string parsing.&lt;/p&gt;

&lt;p&gt;Most JDBC drivers use the text format for simple queries (the "simple query protocol") and only switch to binary for prepared statements. Bpdbi uses the extended query protocol with binary format for &lt;em&gt;everything&lt;/em&gt; — even &lt;code&gt;BEGIN&lt;/code&gt;, &lt;code&gt;COMMIT&lt;/code&gt;, and &lt;code&gt;SET&lt;/code&gt;. This is what makes uniform pipelining possible: every statement uses the same wire protocol, so they can all be batched in a pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Small footprint
&lt;/h3&gt;

&lt;p&gt;Bpdbi's Postgres driver is about 1,400 lines of Java. The whole library is under 200KB and that includes a connection pool.&lt;/p&gt;

&lt;p&gt;Compare that to a typical Postgres/Jdbi/HikariCP stack:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pgjdbc&lt;/code&gt; (JDBC driver)&lt;/td&gt;
&lt;td&gt;~1.1 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jdbi (developer experience)&lt;/td&gt;
&lt;td&gt;~1 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HikariCP (connection pool)&lt;/td&gt;
&lt;td&gt;~160 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~2.3 MB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bpdbi (everything)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&amp;lt; 200 KB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And that's the modest stack. Hibernate is ~15MB, jOOQ is ~15MB.&lt;br&gt;
The Vert.x SQL Client with Netty clocks in at 5MB+.&lt;br&gt;
Bpdbi has no transitive dependencies beyond SCRAM-client (a small crypt lib for connection auth).&lt;/p&gt;

&lt;h3&gt;
  
  
  No Netty, no event loops
&lt;/h3&gt;

&lt;p&gt;Plain &lt;code&gt;java.net.Socket&lt;/code&gt; with unsynchronized buffered I/O streams.&lt;br&gt;
No channel pipelines, no allocator frameworks, no thread pools. This works because Bpdbi connections are single-threaded by design — just like JDBC connections. With virtual threads, blocking on a socket is cheap. The simplicity pays off in readability, debugging, and startup time.&lt;/p&gt;

&lt;h3&gt;
  
  
  GraalVM native-image ready
&lt;/h3&gt;

&lt;p&gt;The core library uses zero reflection. &lt;code&gt;native-image&lt;/code&gt; just works, no configuration needed.&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Developer experience: Jdbi-level, not JDBC-level
&lt;/h2&gt;

&lt;p&gt;Directly using the JDBC API in your application code is low-level and verbose. That's why libraries like &lt;a href="https://jdbi.org" rel="noopener noreferrer"&gt;Jdbi&lt;/a&gt;, &lt;a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/JdbcTemplate.html" rel="noopener noreferrer"&gt;Spring JDBC Template&lt;/a&gt; and &lt;a href="http://sql2o.org" rel="noopener noreferrer"&gt;Sql2o&lt;/a&gt; exist. They provide: named parameters, row mapping, pluggable data type binders/ row mappers/ JSON mappers.&lt;/p&gt;

&lt;p&gt;Bpdbi has all these features built-in: you don't need an additional library. It comes with add-on modules: &lt;code&gt;bpdbi-record-mapper&lt;/code&gt; for mapping rows to Java records, and &lt;code&gt;bpdbi-bean-mapper&lt;/code&gt; for mapping rows to JavaBeans. The &lt;code&gt;bpdbi-kotlin&lt;/code&gt; add-on contains a mapper for mapping rows to Kotlin data classes using &lt;code&gt;kotlinx.serialization&lt;/code&gt; which, unlike the other row mappers, does &lt;strong&gt;not&lt;/strong&gt; use reflection.&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Under the hood
&lt;/h2&gt;

&lt;p&gt;The performance doesn't just come from pipelining. Bpdbi borrows optimization ideas from &lt;code&gt;pgjdbc&lt;/code&gt;, Vert.x, and Jdbi, and even adds some of its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Column-oriented storage.&lt;/strong&gt; A 100K-row, 10-column result creates 10 byte arrays instead of 1,000,000. Each &lt;code&gt;Row&lt;/code&gt; is a lightweight view (buffer reference + row index) with no per-row allocation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lazy decoding.&lt;/strong&gt; Rows store raw wire bytes. Columns you never read are never decoded. Your &lt;code&gt;SELECT *&lt;/code&gt; that only reads 3 columns out of 20? Only those 3 get decoded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binary parameter encoding.&lt;/strong&gt; Sending an &lt;code&gt;int4&lt;/code&gt; as 4 raw bytes instead of the ASCII string &lt;code&gt;"12345"&lt;/code&gt; saves wire bandwidth and server CPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unsynchronized I/O.&lt;/strong&gt; Java's &lt;code&gt;BufferedOutputStream&lt;/code&gt; acquires a lock on every write. Since Bpdbi connections are single-threaded, the buffered streams skip all synchronization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prepared statement cache.&lt;/strong&gt; An LRU cache avoids re-parsing the same SQL. Oversized queries that would flush the cache are rejected outright.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deadlock prevention.&lt;/strong&gt; Large pipelined batches can deadlock if both TCP buffers fill up. Bpdbi estimates response sizes and inserts mid-pipeline syncs to prevent this — transparently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Who is Bpdbi for?
&lt;/h2&gt;

&lt;p&gt;Requirements for using Bpdbi:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;Postgres&lt;/strong&gt; (it's the only database that has pipelines).&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;Java 21+&lt;/strong&gt; (virtual threads make blocking I/O practical at scale).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bpdbi makes a lot of sense if you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Want to use Postgres' &lt;strong&gt;pipelining&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Use RLS (most Supabase users) or otherwise make a lot of "prefix" queries.&lt;/li&gt;
&lt;li&gt;Prefer &lt;strong&gt;simple, blocking code&lt;/strong&gt; over reactive/async/non-blocking code.&lt;/li&gt;
&lt;li&gt;Care about &lt;strong&gt;small dependencies&lt;/strong&gt; and fast startup (GraalVM, serverless, CLI tools).&lt;/li&gt;
&lt;li&gt;Want to &lt;strong&gt;write SQL by hand&lt;/strong&gt; (not ORM).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're already happy with Hibernate or jOOQ and their compile-time SQL validation, Bpdbi is probably not what you need.&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Web application libraries/frameworks that work well with Bpdbi
&lt;/h2&gt;

&lt;p&gt;Bpdbi uses blocking I/O and is designed for virtual threads.&lt;br&gt;
It pairs well with HTTP frameworks that are not mandatorily reactive/async and do not dictate JDBC:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.http4k.org/" rel="noopener noreferrer"&gt;http4k&lt;/a&gt;&lt;/strong&gt; — Functional, zero-reflection, tiny. The philosophical twin
of Bpdbi on the HTTP side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://javalin.io/" rel="noopener noreferrer"&gt;Javalin&lt;/a&gt;&lt;/strong&gt; — Minimal Jetty wrapper with built-in virtual thread support.
Very popular in both Java and Kotlin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://helidon.io/" rel="noopener noreferrer"&gt;Helidon SE&lt;/a&gt; 4+&lt;/strong&gt; — Oracle's lightweight framework. Versions 1–3 were
reactive (Reactive Streams); 4.x was rewritten around virtual threads and blocking I/O.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://undertow.io/" rel="noopener noreferrer"&gt;Undertow&lt;/a&gt;&lt;/strong&gt; — Embedded, low-level. Blocking handlers run on a worker
thread pool (or virtual threads).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://micronaut.io/" rel="noopener noreferrer"&gt;Micronaut&lt;/a&gt;&lt;/strong&gt; — Compile-time DI, GraalVM-first. Supports both reactive
and imperative, controller methods can simply return values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://sparkjava.com/" rel="noopener noreferrer"&gt;Spark&lt;/a&gt;&lt;/strong&gt; — Dead-simple Java micro-framework with the same "just enough" philosophy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://jooby.io/" rel="noopener noreferrer"&gt;Jooby&lt;/a&gt;&lt;/strong&gt; — Modular micro-framework, explicit about dependencies, virtual
thread support.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;com.sun.net.httpserver&lt;/code&gt;&lt;/strong&gt; — The JDK's built-in HTTP server. Zero dependencies, pairs naturally
with Bpdbi's minimalism.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Frameworks like Spring Boot are opinionated about their own data stacks (Spring Data, Hibernate) and assume a JDBC &lt;code&gt;DataSource&lt;/code&gt; integration for transactions, health checks, and connection management.&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Give it a spin!
&lt;/h2&gt;

&lt;p&gt;Everything you need to get started van be found on &lt;a href="https://github.com/bpdbi/bpdbi" rel="noopener noreferrer"&gt;Bpdbi's GitHub repository&lt;/a&gt;. If you miss something, raise an issue to let us know.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Languages in the Linux kernel</title>
      <dc:creator>Cies Breijs</dc:creator>
      <pubDate>Wed, 19 Feb 2025 22:06:22 +0000</pubDate>
      <link>https://dev.to/cies/languages-in-the-linux-kernel-43bf</link>
      <guid>https://dev.to/cies/languages-in-the-linux-kernel-43bf</guid>
      <description>&lt;p&gt;The Linux kernel it written in many languages, but at the time of writing (2025) the main language of the project is C, clocking in at 98% (from &lt;a href="https://github.com/torvalds/linux" rel="noopener noreferrer"&gt;github.com/torvalds/linux&lt;/a&gt; on 2025-02-19):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3cby9ovtpm1cx9a4c0nb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3cby9ovtpm1cx9a4c0nb.png" alt="Language stats of the Linux Kernel" width="800" height="276"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I believe the only language —or more correct "language family"— in the Linux kernel that will always be there is Assembly. At some point you need to write code, without any abstraction, directly for the hardware. Assembly is just that.&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Allowing C++ in the kernel
&lt;/h2&gt;

&lt;p&gt;Linus Torvalds, the &lt;a href="https://en.wikipedia.org/wiki/Benevolent_dictator_for_life" rel="noopener noreferrer"&gt;benevolent dictator for life&lt;/a&gt; of the Linux kernel project, has &lt;a href="https://web.archive.org/web/20080216000358/http://article.gmane.org/gmane.comp.version-control.git/57918" rel="noopener noreferrer"&gt;rejected proposals for allowing C++ in the kernel&lt;/a&gt;. This while C++ has formidable interoperability with C, and was successfully used in many other kernel projects (Google's Fuchsia, Nintendo's Horizon and Nokia's Symbian).&lt;/p&gt;

&lt;p&gt;I believe Linus was right to reject C++ for the following reaons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;C++ is a gigantic language, allowing it would create an endless discussion on which of its features would be allowed/forbidden,&lt;/li&gt;
&lt;li&gt;being a loose superset of C it has the same archaic syntax, thus little improvement to the developer experience, and&lt;/li&gt;
&lt;li&gt;no matter how many features would be allowed C++ does not improve enough on C to offset the cost of porting it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  The end of C
&lt;/h2&gt;

&lt;p&gt;In recent year one thing became clear: C is no longer the best tool for the job. It's still the best understood tool, especially by the current team. It's what 98% of the Linux kernel is written in today. But the "unchallenged best" status it used to have for so many years in pragmatic kernel development is shaky.&lt;/p&gt;

&lt;p&gt;Better languages have come up, specifically &lt;a href="https://en.wikipedia.org/wiki/Zig_(programming_language)" rel="noopener noreferrer"&gt;Zig&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/Rust_(programming_language)" rel="noopener noreferrer"&gt;Rust&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;There has been a lot of discussion on allowing Rust in the kernel. But since the main honchos, Linus and Greg Kroah-Hartman, have put their weight behind it, it is clear that Rust will find it's way into the project. We still have to wait and see how much of the project will be written in Rust.&lt;/p&gt;

&lt;p&gt;Unlike C++, Rust has lots of features that are very useful in kernel development, and very few features that would need to be forbidden. Especially features related to memory safety, which constitute a &lt;a href="https://lore.kernel.org/rust-for-linux/2025021954-flaccid-pucker-f7d9@gregkh" rel="noopener noreferrer"&gt;large part of the bugs in Linux&lt;/a&gt;. Compared to C, Rust provides much a improved developer experience, which is not weird considering Rust is 40 younger.&lt;/p&gt;

&lt;p&gt;Rust's main downside is: slower compile times. Compile times matter very much, but so does safety. While this is a hard trade-off, the decision seems to be final: Rust is to stay in the Linux kernel.&lt;/p&gt;

&lt;p&gt;I expect Zig will come from another angle. The Zig compiler compiles C as well as Zig code. It's just a matter of time before the Zig compiler will be able to compile the Linux kernel. Once this is achieved, C-files can be ported to Zig one-by-one. I expect LLMs will help a great deal with the initial port of the Linux kernel's C code to Zig. Once in Zig, the code can be optimized by humans.&lt;/p&gt;

&lt;p&gt;Zig is very similar to C. This makes the initial port rather straight forward. LLMs will perform much better on C-to-Zig than on C-to-Rust. &lt;/p&gt;

&lt;p&gt;Compared to C, Zig brings serious improvements in the developer experience at similar-to-C compile times. Features like &lt;code&gt;comptime&lt;/code&gt; are really cool and may allow the kernel project to do away with lots of crufty old C preprocessor macros on the one side, while allowing for interesting optimizations on the other.&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I expect we will have a Linux kernel in Zig, Rust and Assembly in 10 years. It may be a fork. It may be the Linux mainline: it all depends on the open-mindedness of the main devs.&lt;/p&gt;

&lt;p&gt;So far they seem up for it!&lt;/p&gt;

</description>
      <category>linux</category>
      <category>c</category>
      <category>zig</category>
      <category>rust</category>
    </item>
    <item>
      <title>The case against ORMs</title>
      <dc:creator>Cies Breijs</dc:creator>
      <pubDate>Wed, 29 Jan 2025 16:41:51 +0000</pubDate>
      <link>https://dev.to/cies/the-case-against-orms-5bh4</link>
      <guid>https://dev.to/cies/the-case-against-orms-5bh4</guid>
      <description>&lt;p&gt;Allow me to kick off with a bold statement: &lt;a href="https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping" rel="noopener noreferrer"&gt;ORM&lt;/a&gt;s —like &lt;a href="https://hibernate.org" rel="noopener noreferrer"&gt;Hibernate&lt;/a&gt; (&lt;a href="https://en.wikipedia.org/wiki/Java_virtual_machine" rel="noopener noreferrer"&gt;JVM&lt;/a&gt;), &lt;a href="https://learn.microsoft.com/en-us/ef" rel="noopener noreferrer"&gt;Entity Framework&lt;/a&gt; (&lt;a href="https://dotnet.microsoft.com" rel="noopener noreferrer"&gt;.NET&lt;/a&gt;), &lt;a href="https://github.com/rails/rails/tree/main/activerecord" rel="noopener noreferrer"&gt;ActiveRecord&lt;/a&gt; (&lt;a href="https://ruby-lang.org" rel="noopener noreferrer"&gt;Ruby&lt;/a&gt;), &lt;a href="https://docs.djangoproject.com/en/stable/topics/db/models" rel="noopener noreferrer"&gt;Django ORM&lt;/a&gt; (&lt;a href="https://www.python.org" rel="noopener noreferrer"&gt;Python&lt;/a&gt;), &lt;a href="https://laravel.com/docs/eloquent" rel="noopener noreferrer"&gt;Eloquent&lt;/a&gt; (&lt;a href="https://laravel.com" rel="noopener noreferrer"&gt;PHP/Laravel&lt;/a&gt;), &lt;a href="https://www.doctrine-project.org" rel="noopener noreferrer"&gt;Doctrine&lt;/a&gt; (&lt;a href="https://www.php.net" rel="noopener noreferrer"&gt;PHP&lt;/a&gt;), &lt;a href="https://gorm.io" rel="noopener noreferrer"&gt;GORM&lt;/a&gt; (&lt;a href="https://go.dev" rel="noopener noreferrer"&gt;Go&lt;/a&gt;), etc.— are &lt;em&gt;never&lt;/em&gt; a good idea. Period.&lt;/p&gt;

&lt;p&gt;Even when using object oriented (OO) languages.&lt;/p&gt;

&lt;p&gt;No matter how good or lacking your SQL skills are.&lt;/p&gt;

&lt;p&gt;Never.&lt;/p&gt;

&lt;p&gt;Why? The short (TLDR) version of this argument is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;They make the simple queries slightly simpler, but they do not help you for the harder queries.&lt;/em&gt; — me&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And with AI coding assistants an ORM's added value is even further reduced.&lt;/p&gt;

&lt;p&gt;For the long version continue reading the rest of this article...&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Simple gets simpler with an ORM!
&lt;/h2&gt;

&lt;p&gt;Instead of the &lt;a href="https://xkcd.com/327" rel="noopener noreferrer"&gt;potentially dangerous&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"select * from User u where u.id = $userId"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can use an ORM and simply write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the following benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Harder to write code vulnerable to SQL injections.&lt;/li&gt;
&lt;li&gt;The result of the ORM call is usually mapped to some &lt;code&gt;User&lt;/code&gt; object, where in case of the SQL you merely get the raw result (a list of tuples).&lt;/li&gt;
&lt;li&gt;Less to type and easier to read! (and code &lt;em&gt;should&lt;/em&gt; be optimized for readability)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But there's more that makes ORMs great! Following associations is really easy when using an ORM. Let's say we want to fetch the user's permissions from the database. Using SQL this would look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"select * from Permission p where p.user_id = $userId"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When using an ORM this could be as simple as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, I believe ORMs greatly simplify &lt;em&gt;saving&lt;/em&gt; and &lt;em&gt;deleting&lt;/em&gt; data in the database. Let's add a permission for the user and then delete the user altogether:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"insert into Permission (user_id, value) values ($userId, $permissionEnum)"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"delete from User u where u.id = $userId"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When using an ORM this would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;Permissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;permissionEnum&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With all these benefits, why do I argue &lt;em&gt;against&lt;/em&gt; using an ORM?&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Disadvantages of ORMs
&lt;/h2&gt;

&lt;p&gt;Every database-backed application at some point needs more complex queries: you will need a query that cannot be expressed using the ORM (like complex aggregations, recursive queries or bulk inserts/updates). And then? You could just retrieve much more data and do the processing in the application layer, which is very slow. Usually in those cases the ORM is bypassed by writing SQL-in-strings, after which the code base contains two ways to query the db: (1) through the ORM and (2) using SQL-in-strings.&lt;/p&gt;

&lt;p&gt;We can conclude ORMs are a &lt;em&gt;very &lt;a href="https://en.wikipedia.org/wiki/Leaky_abstraction" rel="noopener noreferrer"&gt;leaky abstraction&lt;/a&gt;&lt;/em&gt; (thanks Joey). In other words: you cannot merely &lt;em&gt;learn how to use an ORM&lt;/em&gt;, you still need to &lt;em&gt;learn SQL&lt;/em&gt; as well!&lt;/p&gt;

&lt;p&gt;Thus, the whole reason to learn an ORM and the myriad concepts you need to master in order to use that ORM effectively, is just to &lt;em&gt;make the simple queries slightly simpler&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;What are these concepts common to most ORMs? Well...&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Entities&lt;/strong&gt; or &lt;strong&gt;Models&lt;/strong&gt; – For mapping database table rows to objects. This may require quite a bit of coding: every table that you want to query from using the ORM needs some mapping code.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Associations&lt;/strong&gt; – Like one-to-one, one-to-many, and many-to-many. These need to be specified on the models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lazy/eager loading of associations&lt;/strong&gt; – To controlling when related data is fetched.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transactions&lt;/strong&gt; – Ensuring data integrity with commit and rollback operations. Most ORMs have a specific way of achieving this (by abstracting over the db's &lt;code&gt;TRANSACTION&lt;/code&gt;s). &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching&lt;/strong&gt; – Optimizing performance by avoiding redundant queries. Not all ORMs have caching layers, but many do. This also ties into the way your application is clustered.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency&lt;/strong&gt; – Managing conflicts when multiple users update data. Also know as &lt;em&gt;optimistic locking&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity mapping&lt;/strong&gt; – To ensure the same object instance represents the same database row within a session.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sessions&lt;/strong&gt; or &lt;strong&gt;Persistence contexts&lt;/strong&gt; – To manage object life-cycles and database commits efficiently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Life-cycles&lt;/strong&gt; – ORM objects usually life-cycles that tie in with translations, sessions and clean/dirty tracking.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean/dirty tracking&lt;/strong&gt; – data that was not changed, does not need to be updated in the database.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cascading of operations&lt;/strong&gt; – Describes if the associated ORM object should be persisted. This ties in to the ORM,s notion of clean/dirty.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not all these concepts apply to all ORMs. This list is merely to illustrate what concepts you &lt;em&gt;may&lt;/em&gt; have to learn when using an ORM.&lt;/p&gt;

&lt;p&gt;Code that uses an ORM —as the name implies— looks just like any object oriented code. For example, when we want to have retrieve the ISO code of the country that a user's company is registered in, the ORM-based solution would look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;organization&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;registeredAddress&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;country&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;isoCode&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example it is not obvious when the query/queries to the database are made, how many queries are made, and what data is retrieved. With an ORM's lazy/eager settings it could be that fetching a "user" also fetches it's organization. The lazy/eager settings are usually set once: and thus cannot be tweaked per query.&lt;/p&gt;

&lt;p&gt;While the following SQL-in-string alternative is longer, it is also very clear in what value is being retrieved (only the "iso code"):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"""
  select c.isoCode
  from User u
  join Organization o  on u.organizationId = o.id
  join Address a       on o.registrationAddressId = a.id
  join Country c       on a.countryId = c.id
  where u.id = $userId
"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since ORMs tend to hide queries, analyzing performance bottlenecks of ORM-based code is much harder compared to "just SQL".&lt;/p&gt;

&lt;p&gt;When the performance of a relational database backed application is critical, the business logic is usually for some part described in the database queries. In these kind of applications the library used to interact with the database has many call-sites in the application code, and often becomes tightly coupled with the business logic. Changing the library that is used to interact with the database for a large code base quickly becomes prohibitively difficult. Therefor, it is very important to carefully consider your options before making the investment.&lt;/p&gt;

&lt;p&gt;So, when dealing with your data base, would you rather learn both...&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl7fpd3oapccn1bpelphv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl7fpd3oapccn1bpelphv.png" alt="Stack of ORM books" width="450" height="485"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;...and...&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feitx42h8jrigxluwu8vl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feitx42h8jrigxluwu8vl.png" alt="Stack of SQL books" width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;...or just the latter?&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  If not an ORM, then what?
&lt;/h2&gt;

&lt;p&gt;There are other ways to make integrating SQL queries in your language of choice easier. Usually these approaches do &lt;em&gt;not&lt;/em&gt; alleviate you from learning SQL, but &lt;em&gt;may&lt;/em&gt; provide some of the other benefits ORMs are known for, namely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Improved type safety.&lt;/li&gt;
&lt;li&gt;Some checking of SQL queries (so you get compile errors or failing tests when you change the name of a table but forget to update the query accordingly).&lt;/li&gt;
&lt;li&gt;Make it harder to write code vulnerable to SQL injections.&lt;/li&gt;
&lt;li&gt;Reduce boilerplate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These non-ORM options that improve embedding SQL in general purpose languages generally come in two flavors:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Improving on SQL-as-strings.&lt;/li&gt;
&lt;li&gt;As an embedded domain specific language (&lt;a href="https://en.wikipedia.org/wiki/Domain-specific_language#eDSL" rel="noopener noreferrer"&gt;eDSL&lt;/a&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Compared to ORMs both approached are much thinner abstractions. In both cases it is very easy to "see the SQL". The solutions in category two have the potential to provide more features. The solutions in category usually perform slightly better (the eDSL solution are basically query builders: the queries are built at runtime).&lt;/p&gt;

&lt;p&gt;Examples of libraries in the first category are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://jdbi.org" rel="noopener noreferrer"&gt;Jdbi&lt;/a&gt; (Java/JVM),&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cashapp.github.io/sqldelight" rel="noopener noreferrer"&gt;SQLDelight&lt;/a&gt; (&lt;a href="https://kotlinlang.org" rel="noopener noreferrer"&gt;Kotlin&lt;/a&gt;),&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sqlc.dev" rel="noopener noreferrer"&gt;sqlc&lt;/a&gt; (&lt;a href="https://en.wikipedia.org/wiki/C_(programming_language)" rel="noopener noreferrer"&gt;C&lt;/a&gt;),&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/launchbadge/sqlx" rel="noopener noreferrer"&gt;sqlx&lt;/a&gt; (&lt;a href="https://www.rust-lang.org" rel="noopener noreferrer"&gt;Rust&lt;/a&gt;), and&lt;/li&gt;
&lt;li&gt;another library by the same name &lt;a href="https://github.com/jmoiron/sqlx" rel="noopener noreferrer"&gt;sqlx&lt;/a&gt; (&lt;a href="https://go.dev" rel="noopener noreferrer"&gt;Go&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some of these can tie into your build process with a task that generates code based on the database schema (for this to work a database needs to be available at build time). Depending on the chosen library the generated code may contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tables names, column names, columns types and sometimes also index names; to add some type safety and IDE code completion.&lt;/li&gt;
&lt;li&gt;A "record" DTO definition for each (main) table to represents a row in that table.&lt;/li&gt;
&lt;li&gt;Methods to reduce the boilerplate code needed to follow an association.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Examples of solutions in this category are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.jooq.org" rel="noopener noreferrer"&gt;jOOQ&lt;/a&gt; (&lt;a href="https://en.wikipedia.org/wiki/Java_virtual_machine" rel="noopener noreferrer"&gt;JVM&lt;/a&gt;),&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://en.wikipedia.org/wiki/LINQ" rel="noopener noreferrer"&gt;LINQ&lt;/a&gt;'s &lt;a href="https://learn.microsoft.com/en-us/dotnet/csharp/linq/get-started/write-linq-queries" rel="noopener noreferrer"&gt;method syntax&lt;/a&gt; (&lt;a href="https://dotnet.microsoft.com" rel="noopener noreferrer"&gt;.NET&lt;/a&gt;),&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kysely.dev" rel="noopener noreferrer"&gt;Kysley&lt;/a&gt; and &lt;a href="https://orm.drizzle.team/docs/overview#why-sql-like" rel="noopener noreferrer"&gt;Drizzle with SQL-like syntax&lt;/a&gt; (&lt;a href="https://www.typescriptlang.org" rel="noopener noreferrer"&gt;TypeScript&lt;/a&gt;),&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Using an ORM you will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Find it hard or impossible to write efficient queries:

&lt;ul&gt;
&lt;li&gt;easy to write queries in loops,&lt;/li&gt;
&lt;li&gt;often way too much data is being fetched (not easy to reduce this), and&lt;/li&gt;
&lt;li&gt;only a limited set of the database's features are available.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Become locked-in to your ORM library: the API surface is big and it will be used in a large portion of your application's code which results in a large portion of your code needs to be rewritten if you ever want to migrate to another library for accessing the database.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;It is said that ORMs are a good fit for very simple CRUD applications. Many programmers learned about ORMs because they "came with the framework": &lt;a href="https://hibernate.org" rel="noopener noreferrer"&gt;Hibernate&lt;/a&gt; in &lt;a href="https://en.wikipedia.org/wiki/Jakarta_EE" rel="noopener noreferrer"&gt;Java EE&lt;/a&gt; frameworks like &lt;a href="https://spring.io" rel="noopener noreferrer"&gt;Spring&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/ef" rel="noopener noreferrer"&gt;Entity Framework&lt;/a&gt; in (&lt;a href="https://en.wikipedia.org/wiki/ASP.NET" rel="noopener noreferrer"&gt;ASP.NET&lt;/a&gt;), &lt;a href="https://github.com/rails/rails/tree/main/activerecord" rel="noopener noreferrer"&gt;ActiveRecord&lt;/a&gt; (&lt;a href="https://rubyonrails.org" rel="noopener noreferrer"&gt;Ruby on Rails&lt;/a&gt;), &lt;a href="https://docs.djangoproject.com/en/stable/topics/db/models" rel="noopener noreferrer"&gt;Django ORM&lt;/a&gt; in (&lt;a href="https://www.djangoproject.com" rel="noopener noreferrer"&gt;Django&lt;/a&gt;) and &lt;a href="https://www.doctrine-project.org" rel="noopener noreferrer"&gt;Doctrine&lt;/a&gt; (&lt;a href="https://symfony.com" rel="noopener noreferrer"&gt;Symfony&lt;/a&gt;). When the application is small and gets barely any traffic, the ORM does not hurt yet. But as the application grows the ORM becomes a liability.&lt;/p&gt;

&lt;p&gt;It seems to me we have optimized these web frameworks for super clean code "Getting Started" guides. Maybe that's the only thing ORMs are good at: super clean code "Getting Started" guides.&lt;/p&gt;

</description>
      <category>database</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>java</category>
    </item>
  </channel>
</rss>
