{"article":{"slug":"hanami-why-model-citizens","title":"Hanami, Why?: Model Citizens","subtitle":null,"summary":"Aaron Allen's 'Hanami, Why?' series covers Hanami's database layer: why it splits persistence into relations, repos and structs instead of Rails-style models, the advantages of the repo pattern, keeping persistence from leaking into the rest of an app, and the rough edges he has found so far.","content_type":"tutorial","language":"en","canonical_url":"https://aaronmallen.me/writing/hanami-why-model-citizens","author":{"name":"Aaron Allen","url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"Aaron Allen","url":"https://aaronmallen.me/","listing_slug":null,"listing":null},"topics":[{"name":"Web Development","slug":"web-development","url":"https://listedarticles.com/topics/web-development"},{"name":"Software Engineering","slug":"software-engineering","url":"https://listedarticles.com/topics/software-engineering"},{"name":"Databases","slug":"databases","url":"https://listedarticles.com/topics/databases"},{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":2581,"reading_minutes":11,"published_at":"2026-10-19T14:00:00.000Z","added_at":"2026-10-19T14:14:13.179Z","updated_at":"2026-10-19T14:14:13.179Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/hanami-why-model-citizens","markdown_url":"https://listedarticles.com/articles/hanami-why-model-citizens.md","example":false,"citation":"Aaron Allen, Aaron Allen. \"Hanami, Why?: Model Citizens.\" 19 Oct 2026. https://aaronmallen.me/writing/hanami-why-model-citizens (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://aaronmallen.me/writing/hanami-why-model-citizens"},"body_markdown":"# Hanami, Why?: Model Citizens\n\nby Aaron Allen | October 19, 2026\n\nWelcome back to my \"Hanami, Why?\" series. If you missed it, be sure to read [Hanami, Why?: Class Action](https://aaronmallen.me/writing/hanami-why-class-action) before you start this issue. Today we are going to be covering Hanami's database layer. It is important to note the use of the phrase \"database layer,\" as Hanami does not have a traditional \"model\" layer like Rails does. Once again, this series is called \"Hanami, **Why?**\" so just as we did last time, we will cover the basics at a high level and then dive into why things are the way they are. I will warn you that I am still actively forming opinions and learning about Hanami's database layer myself. There is not a day that goes by that [Adam Lassek](https://github.com/alassek) is not feeding me some random TIL about this subject. Shout out to the [Hanakai Discord](https://discord.com/invite/KFCxDmk3JQ); it is a cool place to pick up lots of weird tips and tricks. Let us just dive right in.\n\n## Crash Course\n\nHanami's database layer is composed of three primary parts: [Relations](https://hanakai.org/learn/hanami/v3.0/database/relations), [Repos](https://hanakai.org/learn/hanami/v3.0/database#repositories), and [Structs](https://hanakai.org/learn/hanami/v3.0/database#structs). Relations are essentially where you define an object's schema and relationships to other objects. They are also a great place to define scopes, or what Hanami calls [datasets](https://hanakai.org/learn/hanami/v3.0/database#datasets). Repos are a pretty straightforward representation of the [repository pattern](https://martinfowler.com/eaaCatalog/repository.html). Repos are actually totally optional in Hanami; you could very easily get by with just relations if you were so inclined. Repos do have the advantage of operating on multiple relations and I highly encourage their use. Lastly, structs are simple, frozen representations of your objects themselves. Structs are a place for you to define simple presentation logic for your objects. Let us look at an example of how you would go about utilizing Hanami's database layer.\n\n```\nROM::SQL.migration do\n  change do\n    create_table :users do\n      primary_key :id\n      column :email, :varchar, index: { unique: true }, null: false, size: 255\n      column :password_digest, null: false\n      column :first_name, :text\n      column :last_name, :text\n      column :created_at, :timestamptz, null: false, default: Sequel.function(:now)\n      column :updated_at, :timestamptz, null: false, default: Sequel.function(:now)\n    end\n  end\nend\n```\n\nHere we have a pretty straightforward schema migration. I will not be diving into migrations very much as this is adequately covered in the [Hanami documentation](https://hanakai.org/learn/hanami/v3.0/database/migrations). I am simply including it here so that you can see the schema the `users` table has.\n\n```\nmodule MyApp\n  module Relations\n    class Users < DB::Relation\n      schema :users, infer: true do\n        attribute :email, Types::Normalized::EmailAddress\n        \n        associations do\n          has_many :oauth_tokens\n        end\n      end\n    end\n  end\nend\n```\n\nHere we have our relation. It will infer its schema directly from the users table. We are overriding the `:email` attribute to be a normalized email address. Lastly, we are associating the relation with the `oauth_tokens` table with a `has_many` relationship. ROM, of course, also supports `belongs_to` and `has_one` relationships.\n\n```\nmodule MyApp\n  module Repos\n    class UserRepo < DB::Repo\n      def create(email:, password_digest:, first_name: nil, last_name: nil)\n        users.command(:create).call(email:, password_digest:, first_name:, last_name:)\n      end\n      \n      def find(id)\n        users.by_pk(id).one\n      end\n     \n      def find_by_access_token(token)\n        users.join(oauth_tokens).where(oauth_tokens[:token].is(token)).one\n      end\n      \n      def find_by_email(email)\n        users.where(email:).one\n      end\n    end\n  end\nend\n```\n\nHere we have our `UserRepo` with a few methods to help us find a user, and a method that allows us to create a user. You will notice we have direct access to both the `users` table and the `oauth_tokens` table in our repo.\n\n```\nmodule MyApp\n  module Structs\n    class User < DB::Struct\n      def display_name\n        candidate = full_name\n        candidate.empty? ? email : candidate\n      end\n      \n      def full_name\n        [first_name, last_name].compact.reject(&:empty?).join(\" \")\n      end\n    end\n  end\nend\n```\n\nFinally, here we have our `User` struct with a couple of nice-to-have presentation logic methods. You will notice we can access the `email` and name fields directly without having to explicitly define them in our struct.\n\nThe idea here is that your repos are the access point to the data layer. All methods within the repo **should** return one of your structs. You do this by calling `to_a` on collections and `one` on individual item queries. The command in the `create` method will also return a `User` struct. That is pretty much the whole shebang; congratulations, you now know how to use Hanami's data layer. Let us look at a tiny example of its actual use for completeness.\n\n```\nmodule MyApp\n  module Operations\n    class CreateUser < Operation\n      include Deps[\"argon.hasher\", \"contracts.new_user_contract\", \"repos.user_repo\"]\n      \n      def call(params)\n        validated = step validate_params(params)\n        step validate_email_is_unique(validated[:email])\n        attributes = step build_payload(validated)\n        user_repo.create(**attributes)\n      end\n      \n      private\n      \n      def build_payload(params)\n        built = params.slice(:email, :first_name, :last_name)\n        built[:password_digest] = hasher.call(params[:password])\n        Success(built)\n      rescue Argon2::Error => e\n        Failure(e)\n      end\n      \n      def validate_email_is_unique(email)\n        return Success() if user_repo.find_by_email(email).nil?\n        \n        Failure(:email_taken)\n      end\n      \n      def validate_params(params)\n        result = new_user_contract.call(params)\n        result.success? ? Success(result.to_h[:user]) : Failure(result.errors.to_h)\n      end\n    end\n  end\nend\n```\n\nThere you have it; notice we never reached directly for our relation. We interface purely through the repository layer. Again, this is totally optional and you are free to **just** use relations directly. We will talk about why I think that is a bad idea a little later in this issue.\n\n## Advantages of the Repo Pattern\n\nI will not get into all of the various [f](https://ryanbigg.com/2024/12/scoping-an-active-record-join)[o](https://steveklabnik.com/writing/active-record-considered-harmful/)[o](https://calpaterson.com/activerecord.html)[t](https://damieng.com/blog/2007/activerecord-the-ugly-design-pattern/) [g](https://reinteractive.com/articles/ActiveRecord-callbacks-considered-harmful)[u](https://destroyallsoftware.com/blog/2011/announcing-do-not-want)[n](https://blog.appsignal.com/2018/04/24/active-record-performance-the-n-1-queries-antipattern.html)[s](https://piechowski.io/post/why-is-default-scope-bad-rails/) provided to you by ActiveRecord. Instead I will show you a very simple-to-understand example of a **huge** problem solved by the repository pattern.\n\n```\nclass FreshnessController < ApplicationController\n  def fresh\n    Issue.where(created_at: 7.days.ago..)\n  end\n  \n  def stale\n    Issue.where(created_at: 30.days.ago...7.days.ago)\n  end\n  \n  def expired\n    Issue.where(created_at: ...90.days.ago)\n  end\nend\n```\n\nAlright, we have a pretty simple controller here that determines the \"freshness\" of an issue. Here is the situation: Product has decided that relying on `created_at` for this determination is not working out. Instead they have decided they are going to run a background job every hour and update a new field on the issue called `fresh_status`. Guess what? You now have to update all three of these call sites for this change. Imagine if this were something with dozens or even hundreds of call sites; what a huge pain in the ass. Now I know what you are thinking: \"This is stupid, just use scopes, problem solved.\" To which I 100% agree, because scopes in Rails **are** an implementation of the repository pattern! Hanami encourages you by default to use this pattern to save future you from present you. It is awesome.\n\nSo what does this look like in Hanami? Here is the same freshness logic living where it belongs, in a repo.\n\n```\nmodule MyApp\n  module Repos\n    class IssueRepo < DB::Repo\n      def fresh\n        issues.where(created_at: days_ago(7)..).to_a\n      end\n\n      def stale\n        issues.where(created_at: days_ago(30)...days_ago(7)).to_a\n      end\n\n      def expired\n        issues.where(created_at: ...days_ago(90)).to_a\n      end\n\n      private\n\n      def days_ago(days)\n        Time.now - (days * 86_400)\n      end\n    end\n  end\nend\n```\n\nYour action asks for the repo and calls it. That is it.\n\n```\nmodule MyApp\n  module Actions\n    module Issues\n      class Fresh < MyApp::Action\n        include Deps[\"repos.issue_repo\"]\n\n        def handle(request, response)\n          response.render(view, issues: issue_repo.fresh)\n        end\n      end\n    end\n  end\nend\n```\n\nNow Product shows up with their shiny new `fresh_status` field. Here is the entire change.\n\n```\ndef fresh\n  issues.where(fresh_status: \"fresh\").to_a\nend\n\ndef stale\n  issues.where(fresh_status: \"stale\").to_a\nend\n\ndef expired\n  issues.where(fresh_status: \"expired\").to_a\nend\n```\n\nOne file. Your actions, your views, your mailers, and anything else calling `issue_repo.fresh` never know anything changed. You will also notice there is no `7.days.ago` here; Hanami does not ship with ActiveSupport, so we are doing a little plain Ruby math instead. The real win though is that nobody outside the repo even **has** `issues` to call `where` on, unless they go out of their way to ask for the relation in their `Deps`. The easy path to the database runs through a repo, so the knowledge of what \"fresh\" means lives in exactly one place, and it stays there. This is also why I said earlier that using relations directly is a bad idea. The moment you inject `relations.issues` into an action, you are right back to our `FreshnessController`, with the meaning of \"fresh\" scattered across every file that asked for it.\n\n## Passing Persistence\n\nThere is another, sneakier problem with ActiveRecord, and it has nothing to do with how many call sites you have. Every time you hand someone an ActiveRecord object you hand them the **entire** persistence layer along with it. Take a look at this serializer.\n\n```\nclass IssueSerializer\n  def initialize(issue)\n    @issue = issue\n  end\n\n  def as_json(*)\n    @issue.touch(:last_viewed_at)\n    { title: @issue.title, comments: @issue.comments.map(&:body) }\n  end\nend\n```\n\nHere we have a serializer, whose only job is to turn an issue into JSON, writing to the database and quietly firing off a query for every comment. Nothing stopped it. The serializer was handed a live record, so it can save, update, destroy, and lazy load whatever it wants, and you will not find out until you are staring at your logs at 2 AM. Now let us look at the same thing in Hanami.\n\n```\nmodule MyApp\n  module Repos\n    class IssueRepo < DB::Repo\n      def find_with_comments(id)\n        issues.combine(:comments).by_pk(id).one\n      end\n    end\n  end\nend\n\nissue = issue_repo.find_with_comments(id)\nissue.comments.map(&:body)\nissue.update(last_viewed_at: Time.now) # => ROM::Struct::MissingAttribute\n```\n\nThe repo decides **up front** what data gets loaded, and what comes back is a frozen struct. That struct cannot save itself, it cannot reach back into the database, and it certainly cannot go load more data behind your back. If you want to write something you have to go through a repo, and in Hanami that means you have to explicitly ask for one in your `Deps`. Your views, serializers, and mailers get data and nothing else. This is the whole point; the persistence layer stays in the persistence layer, and everything else gets to read.\n\n## What I Have Learned\n\nUnlike the last issue, where I gave you concrete things to do and things to avoid, we are going to do things slightly differently here. As I stated, I myself am still learning a lot about this aspect of Hanami applications every day. So instead of pretending to be some sort of authority on how you should structure this layer of your application, I would like to tell you a little bit about what I have learned, and some opinions I am holding **very** loosely at the moment. One of the primary friction points I have been experiencing in my adventures with Hanami is that I do not think I like the idea of exporting repositories across slices. Specifically, I am uncomfortable exporting repos that do mutations. I **strongly** feel that all mutations should go into operations. Unfortunately there is not currently a \"read-only\" abstraction in Hanami. Originally I experimented with query objects, but that felt awkward as nine times out of ten the query objects were just delegating to the repo. It was not a great abstraction, and it added unnecessary indirection. So what I ultimately decided to do is split my repos into two. One repo would **only** have read methods in it and it would be called `UserQueries`. The other repo would have all the `create`, `update`, and `delete` mutations and it would be called `UserMutations`. From there it is a matter of convention: `*_queries.rb` files are allowed to be exported and imported by other slices, `*_mutations.rb` are not. I even wrote a [cop](https://github.com/aaronmallen/rubocop-hanami) for this to enforce the convention. Now I am not sure if this is the way I will continue to do things going forward, but for now it seems to be working out pretty great. I have also been told that another approach to this problem is to simply duplicate the relation and struct code in both slice A and slice B and then write the specific repo methods needed for that particular slice. Again, Hanami gives you the freedom to choose how you want to structure your application.\n\n## Rough Edges\n\nI will admit coming from ActiveRecord straight into Sequel and ROM comes with a bit of a learning curve. There are **many** similarities; the query language does not feel foreign. However, there are many small differences as well that take time and toying to really figure out and harness. For example, instead of `MyModel.where(published: true).order(created_at: :desc)` you would need to do `my_model.where(published: true).order { created_at.desc }`. There are dozens of tiny differences in how Sequel does things compared to ActiveRecord. That being said, different does not mean **bad**. I have been able to overcome every obstacle I have run across thus far, and I have confidence you will too.\n\nOne of the other things that bothers me is the concept of timestamps is not really baked into Hanami like they are with Rails. There is no `timestamps` shortcut to call in your migrations. There are some timestamp features baked into ROM, but they are not the best. To ensure that `updated_at` gets incremented you would need to do something like this:\n\n```\nuser.changeset(:update, **attrs).map(:touch).commit\n```\n\nI have two major issues with this:\n\n1. If you forget to call `map(:touch)` in a mutation you are not incrementing your `updated_at` attribute.\n2. ROM hard codes the time that gets populated to `Time.now`, meaning the `updated_at` time is whatever your server time zone is set to.\n\nI tend to extend ROM with the following extensions:\n\n```\nrequire \"rom/changeset\"  \nrequire \"sequel\"  \n  \nmodule MyApp  \n  module Extensions  \n    module ROM  \n      module Changeset  \n        module PipeRegistry  \n          def touch(data)  \n            Hash(updated_at: Sequel.function(:now)).merge(data)  \n          end  \n        end\n      end\n    end\n  end\nend  \n  \nROM::Changeset::PipeRegistry.singleton_class.prepend(MyApp::Extensions::ROM::Changeset::PipeRegistry)\n```\n\n```\nrequire \"my_app/extensions/rom/changeset/pipe_registry\"  \nrequire \"rom/changeset\"  \n  \nROM::Changeset::Update.extend { touch }\n```\n\nThis ensures that the timestamp is whatever the database time is set to (you could alternatively use `Time.now.utc`), and it ensures every update increments the `updated_at` timestamp. I do not view this as a bad thing; I view it as a testament to how easy it is to shape Hanami into what you want it to be.\n\nAlright, that is a wrap on Hanami's database layer. If there is one thing I hope you take away from this issue, it is that the split between relations, repos, and structs is there for a reason. Each piece has exactly one job: relations describe your data, repos decide how you get to it, and structs hand it to you without the ability to wander off and do something stupid with it. That split is what lets you change what \"fresh\" means in one file, and it is what keeps a serializer from writing to your database at 2 AM. As I said at the start, I am still learning about this layer every day, so if you have found a pattern that works better for you I would love to hear about it; come find me in the [Hanakai Discord](https://discord.com/invite/KFCxDmk3JQ). In the next issue we will be covering operations, the place where, as you may have guessed by now, I think all of your mutations belong. I hope to see you there!\n","body_html":"<h1 id=\"hanami-why-model-citizens\">Hanami, Why?: Model Citizens</h1>\n<p>by Aaron Allen | October 19, 2026</p>\n<p>Welcome back to my &quot;Hanami, Why?&quot; series. If you missed it, be sure to read <a href=\"https://aaronmallen.me/writing/hanami-why-class-action\" rel=\"nofollow ugc noopener\">Hanami, Why?: Class Action</a> before you start this issue. Today we are going to be covering Hanami&#39;s database layer. It is important to note the use of the phrase &quot;database layer,&quot; as Hanami does not have a traditional &quot;model&quot; layer like Rails does. Once again, this series is called &quot;Hanami, <strong>Why?</strong>&quot; so just as we did last time, we will cover the basics at a high level and then dive into why things are the way they are. I will warn you that I am still actively forming opinions and learning about Hanami&#39;s database layer myself. There is not a day that goes by that <a href=\"https://github.com/alassek\" rel=\"nofollow ugc noopener\">Adam Lassek</a> is not feeding me some random TIL about this subject. Shout out to the <a href=\"https://discord.com/invite/KFCxDmk3JQ\" rel=\"nofollow ugc noopener\">Hanakai Discord</a>; it is a cool place to pick up lots of weird tips and tricks. Let us just dive right in.</p>\n<h2 id=\"crash-course\">Crash Course</h2>\n<p>Hanami&#39;s database layer is composed of three primary parts: <a href=\"https://hanakai.org/learn/hanami/v3.0/database/relations\" rel=\"nofollow ugc noopener\">Relations</a>, <a href=\"https://hanakai.org/learn/hanami/v3.0/database#repositories\" rel=\"nofollow ugc noopener\">Repos</a>, and <a href=\"https://hanakai.org/learn/hanami/v3.0/database#structs\" rel=\"nofollow ugc noopener\">Structs</a>. Relations are essentially where you define an object&#39;s schema and relationships to other objects. They are also a great place to define scopes, or what Hanami calls <a href=\"https://hanakai.org/learn/hanami/v3.0/database#datasets\" rel=\"nofollow ugc noopener\">datasets</a>. Repos are a pretty straightforward representation of the <a href=\"https://martinfowler.com/eaaCatalog/repository.html\" rel=\"nofollow ugc noopener\">repository pattern</a>. Repos are actually totally optional in Hanami; you could very easily get by with just relations if you were so inclined. Repos do have the advantage of operating on multiple relations and I highly encourage their use. Lastly, structs are simple, frozen representations of your objects themselves. Structs are a place for you to define simple presentation logic for your objects. Let us look at an example of how you would go about utilizing Hanami&#39;s database layer.</p>\n<pre><code>ROM::SQL.migration do\n  change do\n    create_table :users do\n      primary_key :id\n      column :email, :varchar, index: { unique: true }, null: false, size: 255\n      column :password_digest, null: false\n      column :first_name, :text\n      column :last_name, :text\n      column :created_at, :timestamptz, null: false, default: Sequel.function(:now)\n      column :updated_at, :timestamptz, null: false, default: Sequel.function(:now)\n    end\n  end\nend</code></pre>\n<p>Here we have a pretty straightforward schema migration. I will not be diving into migrations very much as this is adequately covered in the <a href=\"https://hanakai.org/learn/hanami/v3.0/database/migrations\" rel=\"nofollow ugc noopener\">Hanami documentation</a>. I am simply including it here so that you can see the schema the <code>users</code> table has.</p>\n<pre><code>module MyApp\n  module Relations\n    class Users &lt; DB::Relation\n      schema :users, infer: true do\n        attribute :email, Types::Normalized::EmailAddress\n        \n        associations do\n          has_many :oauth_tokens\n        end\n      end\n    end\n  end\nend</code></pre>\n<p>Here we have our relation. It will infer its schema directly from the users table. We are overriding the <code>:email</code> attribute to be a normalized email address. Lastly, we are associating the relation with the <code>oauth_tokens</code> table with a <code>has_many</code> relationship. ROM, of course, also supports <code>belongs_to</code> and <code>has_one</code> relationships.</p>\n<pre><code>module MyApp\n  module Repos\n    class UserRepo &lt; DB::Repo\n      def create(email:, password_digest:, first_name: nil, last_name: nil)\n        users.command(:create).call(email:, password_digest:, first_name:, last_name:)\n      end\n      \n      def find(id)\n        users.by_pk(id).one\n      end\n     \n      def find_by_access_token(token)\n        users.join(oauth_tokens).where(oauth_tokens[:token].is(token)).one\n      end\n      \n      def find_by_email(email)\n        users.where(email:).one\n      end\n    end\n  end\nend</code></pre>\n<p>Here we have our <code>UserRepo</code> with a few methods to help us find a user, and a method that allows us to create a user. You will notice we have direct access to both the <code>users</code> table and the <code>oauth_tokens</code> table in our repo.</p>\n<pre><code>module MyApp\n  module Structs\n    class User &lt; DB::Struct\n      def display_name\n        candidate = full_name\n        candidate.empty? ? email : candidate\n      end\n      \n      def full_name\n        [first_name, last_name].compact.reject(&amp;:empty?).join(&quot; &quot;)\n      end\n    end\n  end\nend</code></pre>\n<p>Finally, here we have our <code>User</code> struct with a couple of nice-to-have presentation logic methods. You will notice we can access the <code>email</code> and name fields directly without having to explicitly define them in our struct.</p>\n<p>The idea here is that your repos are the access point to the data layer. All methods within the repo <strong>should</strong> return one of your structs. You do this by calling <code>to_a</code> on collections and <code>one</code> on individual item queries. The command in the <code>create</code> method will also return a <code>User</code> struct. That is pretty much the whole shebang; congratulations, you now know how to use Hanami&#39;s data layer. Let us look at a tiny example of its actual use for completeness.</p>\n<pre><code>module MyApp\n  module Operations\n    class CreateUser &lt; Operation\n      include Deps[&quot;argon.hasher&quot;, &quot;contracts.new_user_contract&quot;, &quot;repos.user_repo&quot;]\n      \n      def call(params)\n        validated = step validate_params(params)\n        step validate_email_is_unique(validated[:email])\n        attributes = step build_payload(validated)\n        user_repo.create(**attributes)\n      end\n      \n      private\n      \n      def build_payload(params)\n        built = params.slice(:email, :first_name, :last_name)\n        built[:password_digest] = hasher.call(params[:password])\n        Success(built)\n      rescue Argon2::Error =&gt; e\n        Failure(e)\n      end\n      \n      def validate_email_is_unique(email)\n        return Success() if user_repo.find_by_email(email).nil?\n        \n        Failure(:email_taken)\n      end\n      \n      def validate_params(params)\n        result = new_user_contract.call(params)\n        result.success? ? Success(result.to_h[:user]) : Failure(result.errors.to_h)\n      end\n    end\n  end\nend</code></pre>\n<p>There you have it; notice we never reached directly for our relation. We interface purely through the repository layer. Again, this is totally optional and you are free to <strong>just</strong> use relations directly. We will talk about why I think that is a bad idea a little later in this issue.</p>\n<h2 id=\"advantages-of-the-repo-pattern\">Advantages of the Repo Pattern</h2>\n<p>I will not get into all of the various <a href=\"https://ryanbigg.com/2024/12/scoping-an-active-record-join\" rel=\"nofollow ugc noopener\">f</a><a href=\"https://steveklabnik.com/writing/active-record-considered-harmful/\" rel=\"nofollow ugc noopener\">o</a><a href=\"https://calpaterson.com/activerecord.html\" rel=\"nofollow ugc noopener\">o</a><a href=\"https://damieng.com/blog/2007/activerecord-the-ugly-design-pattern/\" rel=\"nofollow ugc noopener\">t</a> <a href=\"https://reinteractive.com/articles/ActiveRecord-callbacks-considered-harmful\" rel=\"nofollow ugc noopener\">g</a><a href=\"https://destroyallsoftware.com/blog/2011/announcing-do-not-want\" rel=\"nofollow ugc noopener\">u</a><a href=\"https://blog.appsignal.com/2018/04/24/active-record-performance-the-n-1-queries-antipattern.html\" rel=\"nofollow ugc noopener\">n</a><a href=\"https://piechowski.io/post/why-is-default-scope-bad-rails/\" rel=\"nofollow ugc noopener\">s</a> provided to you by ActiveRecord. Instead I will show you a very simple-to-understand example of a <strong>huge</strong> problem solved by the repository pattern.</p>\n<pre><code>class FreshnessController &lt; ApplicationController\n  def fresh\n    Issue.where(created_at: 7.days.ago..)\n  end\n  \n  def stale\n    Issue.where(created_at: 30.days.ago...7.days.ago)\n  end\n  \n  def expired\n    Issue.where(created_at: ...90.days.ago)\n  end\nend</code></pre>\n<p>Alright, we have a pretty simple controller here that determines the &quot;freshness&quot; of an issue. Here is the situation: Product has decided that relying on <code>created_at</code> for this determination is not working out. Instead they have decided they are going to run a background job every hour and update a new field on the issue called <code>fresh_status</code>. Guess what? You now have to update all three of these call sites for this change. Imagine if this were something with dozens or even hundreds of call sites; what a huge pain in the ass. Now I know what you are thinking: &quot;This is stupid, just use scopes, problem solved.&quot; To which I 100% agree, because scopes in Rails <strong>are</strong> an implementation of the repository pattern! Hanami encourages you by default to use this pattern to save future you from present you. It is awesome.</p>\n<p>So what does this look like in Hanami? Here is the same freshness logic living where it belongs, in a repo.</p>\n<pre><code>module MyApp\n  module Repos\n    class IssueRepo &lt; DB::Repo\n      def fresh\n        issues.where(created_at: days_ago(7)..).to_a\n      end\n\n      def stale\n        issues.where(created_at: days_ago(30)...days_ago(7)).to_a\n      end\n\n      def expired\n        issues.where(created_at: ...days_ago(90)).to_a\n      end\n\n      private\n\n      def days_ago(days)\n        Time.now - (days * 86_400)\n      end\n    end\n  end\nend</code></pre>\n<p>Your action asks for the repo and calls it. That is it.</p>\n<pre><code>module MyApp\n  module Actions\n    module Issues\n      class Fresh &lt; MyApp::Action\n        include Deps[&quot;repos.issue_repo&quot;]\n\n        def handle(request, response)\n          response.render(view, issues: issue_repo.fresh)\n        end\n      end\n    end\n  end\nend</code></pre>\n<p>Now Product shows up with their shiny new <code>fresh_status</code> field. Here is the entire change.</p>\n<pre><code>def fresh\n  issues.where(fresh_status: &quot;fresh&quot;).to_a\nend\n\ndef stale\n  issues.where(fresh_status: &quot;stale&quot;).to_a\nend\n\ndef expired\n  issues.where(fresh_status: &quot;expired&quot;).to_a\nend</code></pre>\n<p>One file. Your actions, your views, your mailers, and anything else calling <code>issue_repo.fresh</code> never know anything changed. You will also notice there is no <code>7.days.ago</code> here; Hanami does not ship with ActiveSupport, so we are doing a little plain Ruby math instead. The real win though is that nobody outside the repo even <strong>has</strong> <code>issues</code> to call <code>where</code> on, unless they go out of their way to ask for the relation in their <code>Deps</code>. The easy path to the database runs through a repo, so the knowledge of what &quot;fresh&quot; means lives in exactly one place, and it stays there. This is also why I said earlier that using relations directly is a bad idea. The moment you inject <code>relations.issues</code> into an action, you are right back to our <code>FreshnessController</code>, with the meaning of &quot;fresh&quot; scattered across every file that asked for it.</p>\n<h2 id=\"passing-persistence\">Passing Persistence</h2>\n<p>There is another, sneakier problem with ActiveRecord, and it has nothing to do with how many call sites you have. Every time you hand someone an ActiveRecord object you hand them the <strong>entire</strong> persistence layer along with it. Take a look at this serializer.</p>\n<pre><code>class IssueSerializer\n  def initialize(issue)\n    @issue = issue\n  end\n\n  def as_json(*)\n    @issue.touch(:last_viewed_at)\n    { title: @issue.title, comments: @issue.comments.map(&amp;:body) }\n  end\nend</code></pre>\n<p>Here we have a serializer, whose only job is to turn an issue into JSON, writing to the database and quietly firing off a query for every comment. Nothing stopped it. The serializer was handed a live record, so it can save, update, destroy, and lazy load whatever it wants, and you will not find out until you are staring at your logs at 2 AM. Now let us look at the same thing in Hanami.</p>\n<pre><code>module MyApp\n  module Repos\n    class IssueRepo &lt; DB::Repo\n      def find_with_comments(id)\n        issues.combine(:comments).by_pk(id).one\n      end\n    end\n  end\nend\n\nissue = issue_repo.find_with_comments(id)\nissue.comments.map(&amp;:body)\nissue.update(last_viewed_at: Time.now) # =&gt; ROM::Struct::MissingAttribute</code></pre>\n<p>The repo decides <strong>up front</strong> what data gets loaded, and what comes back is a frozen struct. That struct cannot save itself, it cannot reach back into the database, and it certainly cannot go load more data behind your back. If you want to write something you have to go through a repo, and in Hanami that means you have to explicitly ask for one in your <code>Deps</code>. Your views, serializers, and mailers get data and nothing else. This is the whole point; the persistence layer stays in the persistence layer, and everything else gets to read.</p>\n<h2 id=\"what-i-have-learned\">What I Have Learned</h2>\n<p>Unlike the last issue, where I gave you concrete things to do and things to avoid, we are going to do things slightly differently here. As I stated, I myself am still learning a lot about this aspect of Hanami applications every day. So instead of pretending to be some sort of authority on how you should structure this layer of your application, I would like to tell you a little bit about what I have learned, and some opinions I am holding <strong>very</strong> loosely at the moment. One of the primary friction points I have been experiencing in my adventures with Hanami is that I do not think I like the idea of exporting repositories across slices. Specifically, I am uncomfortable exporting repos that do mutations. I <strong>strongly</strong> feel that all mutations should go into operations. Unfortunately there is not currently a &quot;read-only&quot; abstraction in Hanami. Originally I experimented with query objects, but that felt awkward as nine times out of ten the query objects were just delegating to the repo. It was not a great abstraction, and it added unnecessary indirection. So what I ultimately decided to do is split my repos into two. One repo would <strong>only</strong> have read methods in it and it would be called <code>UserQueries</code>. The other repo would have all the <code>create</code>, <code>update</code>, and <code>delete</code> mutations and it would be called <code>UserMutations</code>. From there it is a matter of convention: <code>*_queries.rb</code> files are allowed to be exported and imported by other slices, <code>*_mutations.rb</code> are not. I even wrote a <a href=\"https://github.com/aaronmallen/rubocop-hanami\" rel=\"nofollow ugc noopener\">cop</a> for this to enforce the convention. Now I am not sure if this is the way I will continue to do things going forward, but for now it seems to be working out pretty great. I have also been told that another approach to this problem is to simply duplicate the relation and struct code in both slice A and slice B and then write the specific repo methods needed for that particular slice. Again, Hanami gives you the freedom to choose how you want to structure your application.</p>\n<h2 id=\"rough-edges\">Rough Edges</h2>\n<p>I will admit coming from ActiveRecord straight into Sequel and ROM comes with a bit of a learning curve. There are <strong>many</strong> similarities; the query language does not feel foreign. However, there are many small differences as well that take time and toying to really figure out and harness. For example, instead of <code>MyModel.where(published: true).order(created_at: :desc)</code> you would need to do <code>my_model.where(published: true).order { created_at.desc }</code>. There are dozens of tiny differences in how Sequel does things compared to ActiveRecord. That being said, different does not mean <strong>bad</strong>. I have been able to overcome every obstacle I have run across thus far, and I have confidence you will too.</p>\n<p>One of the other things that bothers me is the concept of timestamps is not really baked into Hanami like they are with Rails. There is no <code>timestamps</code> shortcut to call in your migrations. There are some timestamp features baked into ROM, but they are not the best. To ensure that <code>updated_at</code> gets incremented you would need to do something like this:</p>\n<pre><code>user.changeset(:update, **attrs).map(:touch).commit</code></pre>\n<p>I have two major issues with this:</p>\n<ol><li>If you forget to call <code>map(:touch)</code> in a mutation you are not incrementing your <code>updated_at</code> attribute.</li><li>ROM hard codes the time that gets populated to <code>Time.now</code>, meaning the <code>updated_at</code> time is whatever your server time zone is set to.</li></ol>\n<p>I tend to extend ROM with the following extensions:</p>\n<pre><code>require &quot;rom/changeset&quot;  \nrequire &quot;sequel&quot;  \n  \nmodule MyApp  \n  module Extensions  \n    module ROM  \n      module Changeset  \n        module PipeRegistry  \n          def touch(data)  \n            Hash(updated_at: Sequel.function(:now)).merge(data)  \n          end  \n        end\n      end\n    end\n  end\nend  \n  \nROM::Changeset::PipeRegistry.singleton_class.prepend(MyApp::Extensions::ROM::Changeset::PipeRegistry)</code></pre>\n<pre><code>require &quot;my_app/extensions/rom/changeset/pipe_registry&quot;  \nrequire &quot;rom/changeset&quot;  \n  \nROM::Changeset::Update.extend { touch }</code></pre>\n<p>This ensures that the timestamp is whatever the database time is set to (you could alternatively use <code>Time.now.utc</code>), and it ensures every update increments the <code>updated_at</code> timestamp. I do not view this as a bad thing; I view it as a testament to how easy it is to shape Hanami into what you want it to be.</p>\n<p>Alright, that is a wrap on Hanami&#39;s database layer. If there is one thing I hope you take away from this issue, it is that the split between relations, repos, and structs is there for a reason. Each piece has exactly one job: relations describe your data, repos decide how you get to it, and structs hand it to you without the ability to wander off and do something stupid with it. That split is what lets you change what &quot;fresh&quot; means in one file, and it is what keeps a serializer from writing to your database at 2 AM. As I said at the start, I am still learning about this layer every day, so if you have found a pattern that works better for you I would love to hear about it; come find me in the <a href=\"https://discord.com/invite/KFCxDmk3JQ\" rel=\"nofollow ugc noopener\">Hanakai Discord</a>. In the next issue we will be covering operations, the place where, as you may have guessed by now, I think all of your mutations belong. I hope to see you there!</p>","headings":[{"level":1,"text":"Hanami, Why?: Model Citizens","id":"hanami-why-model-citizens"},{"level":2,"text":"Crash Course","id":"crash-course"},{"level":2,"text":"Advantages of the Repo Pattern","id":"advantages-of-the-repo-pattern"},{"level":2,"text":"Passing Persistence","id":"passing-persistence"},{"level":2,"text":"What I Have Learned","id":"what-i-have-learned"},{"level":2,"text":"Rough Edges","id":"rough-edges"}]}}