In one of my projects, we use service objects for things that are otherwise uncomfortable with the ActiveRecord pattern. There are many reasons why something might be uncomfortable with the ActiveRecord pattern, but the most frequently seen reason is for an attribute that isn’t stored, but used to calculate another stored attribute. We’ll look at what ActiveModel::Attributes is, and what we can do with it.
How ActiveModel::Attributes works
ActiveModel::Attributes is used under the hood of ActiveRecord (your database driver) to cast data from your database into Ruby objects. Example: If you’re storing decimals in postgres, you need to cast them to BigDecimal. ActiveModel is what handles this under the hood.
ActiveRecord is smart enough to know what columns the model’s table has, so it creates getters and setters on behalf of the database so you can use familiar Ruby idioms for your database. To achieve this, it uses its
So, if you had a
Post table with columns of
body, ActiveRecord would automatically generate something like this:
This allows you to do
And then, when you want to save or update an object to the database, ActiveRecord looks at the columns available to it and smartly picks those attributes for sending to your database. This means we can have attrbiutes that act like database-backed columns but aren’t.
A real-world example
All good things need a real-world example. We’ll look at how I handle credit cards for TinyVoicemail.
TinyVoicemail uses Stripe for payment processing and subscription management via Stripe Billing. It’s a great product run by great people. Using Stripe doesn’t require you to be PCI-compliant, as they allow you to generate single-use tokens that, once consumed, provide a Stripe-backed credit card object. Stripe’s implementation is worth its own post so I won’t go into too much detail.
Below is a truncated version of the
CreditCard model used by TinyVoicemail (and an upcoming open-source SaaS framework!). We’ll go through the relevant goodies that’s provided by ActiveModel to create Stripe-backed credit cards.
In the controller that handles credit cards, to create a new card, we simply (briefly),
token is has a getter and setter defined on our CreditCard model via ActiveModel::Attributes. It’s not backed by the database, so we really don’t care about it on an instance-by-instance basis.
Then, we use it do set attributes that we do care about before we validate the object.
create_stripe_card! method, we use Stripe’s Ruby SDK to create our card.
Of note, we can access the
token we defined earlier just how we would any other database-backed attribute.
Inside this method, we rescue from errors and apply them as necessary to the object, applying errors to our object that act like database-backed columns too.
Assuming everything goes to plan, we’ll persist this object to our database and we’ll never see our beloved token again — because we don’t have any use for it. I personally find this approach to be a lot cleaner than using a separate class to create this object and provide feedback to users.