The Accelerate HR Blog
Rake up with a fat model (Fri Nov 02 2007)
The Rails gurus are always telling us how we should learn to love fat models. Jamis Buck, for example, is in love with the aesthetics. With all the business logic in your models, he says, your views can be pure shining HTML and your controllers can be positively anorexic (well, the word he uses is 'skinny')
But having blindly followed the experts' advice, I've suddenly realized that it's not just about the beauty and readability of your code. Not at all. My fat models have just saved me a couple of weeks of work.
Here's the scenario. My HR database has a pretty complex, deeply interwoven structure. Let's give a single example. We've got people. And people have jobs, and jobs belong to departments and the departments are in different locations, and several locations - possibly in different countries - may belong to a single business. The jobs also come fitted with grades which are generally standard for the business. And the grades have standard benefits (salary range, vacation days per year, paid overtime, transport, etc), but these may differ between the business's various locations. The benefits are also going to be affected by the employee's contract status: for example if you've been hired from overseas (and the majority of the employees in Mid-East construction are!) you may be entitled to vacation tickets and company accommodation; if you're on a married status contract, you'd get family accommodation; if you're hired from a sub-contractor, none of the benefits would apply.
So when a new employee is added to the database, we want the correct benefits information to be added to the person's record automatically. Of course, there may be special arrangements for some people. But if we can fill the record with the details that will be correct in most cases, we'll save everyone lots of tedious data-entry work later.
The temptation was to build all the logic for this in the employee Controller. After all, when you add a new person, there's going to be a Create action, and the benefits will be entered as the record is created.
But I didn't do it like that, Because Jamis told me to put the logic in my models, not in my controllers. And generally I try to follow his advice. Boy, am I glad I did!
I came to the point where I was ready to migrate a full set of employee details for over 400 people from the existing database and a number of loosely attached Excel sheets. And I wasn't relishing the thought. On my old desktop version I've written a number of data migration routines. But when it came down to it, most of the migrations I ever did for new clients used to involve a lot of manual grunt-work. Sure, I had relationships between my tables, and plenty of cascade instructions. But my business logic was everywhere: in modules attached to forms, in independent functions, in sql queries ...
But with ACCELERATE HR, I've done it the Rails way and put all my logic in one place - the model. And this means I can bring Rake into play, and get everything updated with a single command.
Until now my use of Rake was limited to rake db:migrate when I create a new model and want to add it to the database schema. But it was time to learn more and I went back to Chad Fowler's Rails Recipes - which in my early days with Rails, I'd struggled with, but now I've learned more, is an increasingly valuable resource. Recipe 48 in the book seemed to give me what I was looking for: "Rake, like make before it, is a tool whose primary purpose is to automate software builds."
So I tried out the recipe, and for importing employee data, came up with this, created as #{Rails Root}/lib/tasks/load_employees.rake:
desc "Load new employees for location into database."
task :load_employees => ["#{RAILS_ROOT}/lib/employees.csv",
:environment] do |t|
:last_name => last_name,
:gender => gender,
:nationality_id => nationality_id,
:staff_number => staff_number,
:job_id => job_id,
:hired_by_business => hired_by_business,
:date_of_hire => date_of_hire,
:contract_id => contract_id,
:has_family_members => has_family_members,
:destination_id => destination_id,
:leaving_date => leaving_date,
:inactive => inactive,
:business_id => business_id,
:location_id => location_id
)
puts "Loaded #{Employee.count - before_count } entries."
I won't dwell on how Rake works - there are a couple of references below if you want to learn more. But it's important to include :environment in the task line - that tells Rails how to connect to the right database. And the before_count instruction and puts line are a nice little addition: after running your Rake command you'll be told how many records you've added so that you can check the number against your original .csv file and make sure everything's been properly added.
Which brings me to the .csv file containing the original records and added as #{Rails Root}/lib/file_name.rake:. You simply need to create your .csv file - in this case without headers - with the same field layout as your Rake definition.
I said simply. In my case it wasn't quite that simple. My data came from a number of sources, which had to be built into a single file. To make the job easier, I resorted to (Shock! Horror!) Microsoft Access. Well, that's the database I've been using since it was a baby and I wasn't quite so - and I still find it the easiest way to cope with rapid query building and file transfers to and from a spreadsheet (or in this case a .csv file). But since this is about Rails, not Access, I won't bore you with the details of how I did it. Suffice it to say that I created the tables I needed in Access, exported them to Excel, and from there created the .csv files for my lib folder.
(I should just mention a gotcha though, if like me, you're working in a Windows environment (- there, now my Rails credibility is really blown!). Before I converted from Excel to .csv, I had to make sure that all dates were in a yyyy/mm/dd format. And any time I opened the .csv file I found that the date format had reverted and I had to change it back to yyyy/mm/dd. This may be because my system is set to a European not a US date format - or it may be just a vagary of working with .csv files from an Excel host format. Anyway, check your dates before you run Rake: look at the .csv file in a text format and make sure you're seeing yyyy-mm-dd.)
Now, let's go back to Rake. Notice that in the list of fields, there's absolutely no reference to employee benefits. Nor do we include the grade, to which the benefits are tied. And yet when I run rake load_employees, everyone has a set of default benefits automatically assigned. Why? You already know. It's because I've defined the business logic in the employee Model. I simply added an after_create method, naming it build_associated_tables. And in a protected area of the model, build_associated_tables defines exactly what's going to happen.
Would this have worked if I'd put the logic in the employee Controller. Absolutely not. My Rake task doesn't call any employee actions - doesn't need to. It simply builds the database by calling on the models.
A couple of hours after I'd imported all the data I had, the client mailed me. We've got a lot more data on spreadsheets you haven't seen yet. More passport details, visa numbers, expiry dates. Can we just send you the Excel files? Absolutely .. and that's one of the other great features of Rake. If you add data to an existing .csv file, then the next time you run the Rake command, it only migrates the new records. So I didn't just save time with the initial migration. It'll be something I can use again and again – and not just with this client but also any others who want a quick way to populate their database.
And that's why I'm going to make it a habit always to rake up with a fat model.
REFERENCES:
Jamis Buck on Fat Models.
If you don't know The Rails Way, check this out too, Jamis and another member of the Rails Core team, Michael Koziarski, show us how it should be done.
Chad Fowler's Rails Recipes. Sorry, afraid you need to buy the book - but here's a reference.
The Rails Envy team tell you everything you need to know about Rake - including how to become an alcoholic in a series of simple Rake tasks.