Intelligent ActiveRecord Models

ActiveRecord models in Rails already do a lot of the heavy lifting, in terms of database access and model relationships, but with a bit of work, they can do more things automatically. Let's find out how!


Step 1 - Create a Base Rails App

This idea works for any sort of ActiveRecord project; however, since Rails is the most common, we'll be using that for our example app. The app we'll be using has lots of Users, each of whom can perform a number of actions on Projects .

If you've never created a Rails app before, then read this tutorial, or syllabus, first. Otherwise, fire up the old console and type rails new example_app to create the app and then change directories to your new app with cd example_app.


Step 2 - Create Your Models and Relationships

First, we generate the user that will own:

Likely, in a real world project, we'd have a few more fields, but this will do for now. Let's next generate our project model:

We then edit the generated project.rb file to describe the relationship between users and projects:

and the reverse relationship in user.rb:

Next, run a quick rake db:migrate, and we're ready to begin getting intelligent with these models. If only getting relationships with models was as easy in the real world! Now, if you've ever used the Rails framework before, you've probably learned nothing... yet!


Step 3 - Faux Attributes Are Cooler Than Faux Leather

The first thing we're going to do is use some auto generating fields. You'll have noticed that when we created the model, we created a password hash and not a password field. We're going to create a faux attribute for a password that will convert it to a hash if it's present.

So, in your model, we'll add a definition for this new password field.

We only store a hash against the user so we're not giving out the passwords without a bit of a fight.

The second method means we return something for forms to use.

We also need to ensure that we have the Sha1 encryption library loaded; add require 'sha1' to your application.rb file after line 40: config.filter_parameters += [:password].

As we've changed the app at the configuration level, reload it with a quick touch tmp/restart.txt in your console.

Now, let's change the default form to use this instead of password_hash. Open _form.html.erb in the app/models/users folder:

becomes

We'll make it an actual password field when we're happy with it.

Now, load http://localhost/users and have a play with adding users. It should look a bit like the image below; great, isn't it!

User Form

Wait, what's that? It overwrites your password hash every time you edit a user? Let's fix that.

Open up user.rb again, and change it like so:

This way, only when you supply a password does the field get updated.


Step 4 - Automatic Data Guarantees Accuracy or Your Money Back

The last section was all about changing the data that your model gets, but what about adding more information based on things already known without having to specify them? Let's have a look at that with the project model. Begin by having a look at http://localhost/projects.

Make the following changes quickly.

*app/controllers/projects_controler.rb* line 24

*app/views/projects/_form.html.erb* line 24

*app/views/projects/_form.html.erb* line 24

In MVC frameworks, the roles are clearly defined. Models represent the data. Views display the data. Controllers get data and pass them to the view.

Who Enjoys Filling Out Date/time Fields?

We now have a full functioning form, but it bugs me that I have to set the start_at time manually. I'd like to have it set when I assign a started_by user. We could put it in the controller, however, if you've ever heard the phrase "fat models, skinny controllers" you'll know this makes for bad code. If we do this in the model, it'll work anywhere we set a the starter or completer. Let's do that.

First edit app/models/project.rb, and add the following method:

This code ensures that something has actually been passed. Then, if it's a user, it retrieves its ID and finally writes both the user *and* the time it happened - holy smokes! Let's add the same for the completed_by field.

Now edit the form view so we don't have those time selects. In app/views/projects/_form.html.erb, remove lines 26-29 and 18-21.

Open up http://localhost/projects and have a go!

Spot the Deliberate Mistake

Whoooops! Someone (I'll take the heat since it's my code) cut and paste, and forgot to change the :started_at to :completed_at in the second largely identical (hint) attribute method. No biggie, change that and everything is go… right?


Step 5 - Help Your Future Self by Making Additions Easier

So apart from a little cut-and-paste confusion, I think we did fairly good job, but that slip up and the code around it bothers me a bit. Why? Well, let's have a think:

  • It's cut and paste duplication: DRY (Don't repeat yourself) is a principle to follow.
  • What if someone wants to add another somethingd_at and somethingd_by to our project, like, say, authorised_at and authorised_by>
  • I can imagine quite a few of these fields being added.

Lo and behold, along comes a pointy haired boss and asks for, {drumroll}, authorised_at/by field and a suggested_at/by field! Right then; let's get those cut and paste fingers ready then... or is there a better way?

The Scary Art of Meta-progamming!

That's right! The holy grail; the scary stuff your mothers warned you about. It seems complicated, but actually can be pretty simple - especially what we're going to attempt. We're going to take an array of the names of stages we have, and then auto build these methods on the fly. Excited? Great.

Of course, we'll need to add the fields; so let's add a migration rails generate migration additional_workflow_stages and add those fields inside the newly generated db/migrate/TODAYSTIMESTAMP_additional_workflow_stages.rb.

Migrate your database with rake db:migrate, and replace the projects class with:

I've left the started_by in there so you can see how the code was before.

Nice and gentle - goes through the names(ish) of the methods we wish to create:

For each of those names, we work out the two model attributes we're setting e.g started_by_id and started_at and the name of the association e.g. starter

This seems pretty familiar. This is actually a Rails bit of metaprogramming already that defines a bunch of methods.

Ok, we come to some real meta programming now that calculates the 'get method' name - e.g. started_by, and then creates a method, just as we do when we write def method, but in a different form.

A little bit more complicated now. We do the same as before, but this is the set method name. We define that method, using define(method_name) do |param| end, rather than def method_name=(param).

That wasn't so bad, was it?

Try it Out in the Form

Let's see if we can still edit projects as before. It turns out that we can! So we'll add the additional fields to the form, and, hey, presto!

app/views/project/_form.html.erb line 20

And to the show view... so we can see it working.

*app/views-project/show.html.erb* line 8

Have another play with http://localhost/projects, and you can see we have a winner! No need to fear if someone asks for another workflow step; simply add the migration for the database, and put it in the array of methods... and it gets created. Time for a rest? Maybe, but I've just two more things to make note of.


Step 6 - Automate the Automation

That array of methods seems quite useful to me. Could we do more with it?

First, let's make the list of method names a constant so we can access it from outside.

Now, we can use them to auto create form and views. Open up the _form.html.erb for projects, and let's try it by replacing lines 19 -37 with the snippet below:

But app/views-project/show.html.erb is where the real magic is:

This should be fairly clear, although, if you're not familiar with send(), it's another way to call a method. So object.send("name_of_method") is the same as object.name_of_method.

Final Sprint

We're almost done, but I've noticed two bugs: one is formatting, and the other is a bit more serious.

The first is that, while I view a project, the whole method is showing an ugly Ruby object output. Rather than adding a method to the end, like this

Let's modify User to have a to_s method. Keep things in the model if you can, and add this to the top of the user.rb, and do the same for project.rb as well. It always makes sense to have a default representation for a model as a string:

Feels a bit mundane writing methods the easy way now, eh? No? Anyhow, on to more serious things.

An Actual Bug

When we update a project because we send all of the workflow stages that have been assigned previously, all our time stamps are mixed up. Fortunately, because all our code is in one place, a single change will fix them all.


Conclusion

What have we learned?

  • Adding functionality to the model can seriously improve the rest of you code
  • Meta programming isn't impossible
  • Suggesting a project might get logged
  • Writing smart in the first place means less work later
  • No-one enjoys cutting, pasting and editing and it causes bugs
  • Smart Models are sexy in all walks of life

Thank you so much for reading, and let me know if you have any questions.

Tags:

Comments

Related Articles