How to design and prep a Ruby on Rails model architecture
[Author's note: I wrote the first couple dozen tutorials in this series a few years ago and I'm in the process of updating the content to reflect the evolution of best practices in the industry. Please comment if you see anything I missed that should be updated. Thanks!]
FIRST THINGS FIRST: LIST OUT YOUR MODELS
The cool thing about writing code in general, and Rails in particular, is that you don't need to get this nailed from the start. Through the processes of developing your app you will add/remove models and add/remove/modify columns (i.e. attributes you'll be storing in a database) very often.
At this stage in the game I'd recommend quickly list out the model names. You should be in Minimum Viable Product (MVP) mode here, so don't add more models than you absolutely need to as you start. Many devs like to use Unified Modeling Language (UML) modeling tools to build out their models (attributes and relationships), but I prefer to make a simple list — get it in the code ASAP — and iterate from there.
Here is the list of models we'll be using (to start) in our example app (you should be working on your own app idea in parallel with this tutorial series to get the most out of it):
User Meeting AgendaItem ActionItem Comment
At this point you should have questions whether or not to add more. In our example app, for example, do we need attachments to our Action Items and/or Agenda Items? This is where the rubber meets the road for your MVP and where Lean philosophies turn into practice.
Here is a question we can ask ourselves: Will our product still be viable without an attachments feature? In other words, will we still be able to test our core value and growth hypotheses without it? Remember, our value hypothesis is that users want a simple app that organizes our 4 main resources, and the growth hypothesis is that users will directly invite other users in to use it.
Based on this analysis, then, the answer is no: we can build out an attachment feature later if users want it. It's better to ship early and often. Build → Ship → Test → Evaluate → Repeat.
Rails has a nice semantic way to declare model associations (you should pause for a moment and read about them here). We'll go ahead and use the association methods in our quick list and copy/paste them into our model files later below:
User has_and_belongs_to_many :meetings has_many :action_items has_many :agenda_items has_many :comments Meeting has_and_belongs_to_many :users has_many :agenda_items has_many :action_items AgendaItem belongs_to :user belongs_to :meeting has_many :comments, :as => :commentable ActionItem belongs_to :user belongs_to :meeting has_many :comments, :as => :commentable Comment belongs_to :commentable, :polymorphic => true belongs_to :user
Pretty much every app has interesting relationships that can be tricky to nail to start. Every beginner ends up reversing the has_many/belongs_to placements at some point, for example, or gets the syntax wrong for a polymorphic association (i.e. that Agenda Items and Action Items can both have comments). Rails will give you a helpful error when you do this, so you can always go in and fix it (← this is the day-to-day life of a dev).
It's difficult to over-emphasize the importance of thinking carefully about these model relationships. This is where a dev earns their wage; the design of the database dramatically affects both the speed of the app and the simplicity of the code. The conventions of Rails are super helpful here, but they can only get you so far. Keep it simple.
For example, I could add a “follower” feature between Users and Action Items/Agenda Items, and if I'm not careful I could end up adding more relationships that are necessary for my MVP, or ones that would be sloppy and slow. We'll think about this later and implement the feature if our users need it.
GENERATE YOUR RAILS APP AND SET YOUR MODEL ATTRIBUTES
If you've been following along in this tutorial series then you may have already generated a Rails app in the first “Hello World” post. Go ahead and move or rename that folder and let's start fresh:
~/apps $ gem install rails --no-ri --no-rdoc ~/apps $ rails new OurAgendaApp -d postgresql
At this point let's go ahead and create scaffolds (← give this a quick read if you don't know what scaffolds are) for our models that we listed above. Pick one attribute per model that you know will be part of it, the rest we'll add later (here are the supported data types; we use ‘string', ‘datetime', and ‘text' here).
~/apps $ cd OurAgendaApp ~/apps/OurAgendaApp $ rails g scaffold User first_name:string . . . #will generate a bunch of files ~/apps/OurAgendaApp $ rails g scaffold AgendaItem title:string . . . ~/apps/OurAgendaApp $ rails g scaffold ActionItem title:string . . . ~/apps/OurAgendaApp $ rails g scaffold Meeting starts_on:datetime . . . ~/apps/OurAgendaApp $ rails g scaffold Comment body:text . . .
Ok cool, now let's go ahead and fill out the attributes and other migration-related aspects of our app in our migration files. Look in the db/migrate folder and you should see 5 files. Let's go ahead and open each one and fill in more attributes:
#db/migrate/XXXXXX_create_users.rb class CreateUsers < ActiveRecord::Migration def change create_table :users do |t| t.string :first_name t.string :last_name t.string :email t.string :password_digest #for storing crypted passwords t.timestamps #default created_at and updated_at attributes end #adding a join table for our #has_and_belongs_to_many relationship #model order needs to be alphabetical create_table :meetings_users do |t| t.integer :meeting_id t.integer :user_id end #add an index to speed queries up add_index(:meetings_users, [:meeting_id, :user_id]) end end
Migrations are awesome because Rails handles the SQL to different databases (e.g. MySQL vs. PostgreSQL) appropriately. Let's go ahead and repeat this process for our 4 other models:
#db/migrate/XXXXXX_create_agenda_items.rb class CreateAgendaItems < ActiveRecord::Migration def change create_table :agenda_items do |t| t.string :title t.text :description t.integer :meeting_id #this links it to a meeting t.integer :user_id #the user who made it t.timestamps end add_index :agenda_items, :meeting_id #don't forget these add_index :agenda_items, :user_id end end #db/migrate/XXXXXX_create_action_items.rb class CreateActionItems < ActiveRecord::Migration def change create_table :action_items do |t| t.string :title t.text :description t.integer :user_id t.integer :meeting_id t.timestamps end add_index :action_items, :meeting_id add_index :action_items, :user_id end end #db/migrate/XXXXXX_create_meetings.rb class CreateMeetings < ActiveRecord::Migration def change create_table :meetings do |t| t.datetime :starts_on t.datetime :ends_on t.string :name t.string :location t.timestamps end end end #db/migrate/XXXXXX_create_comments.rb class CreateComments < ActiveRecord::Migration def change create_table :comments do |t| t.text :body t.references :commentable, :polymorphic => true t.integer :user_id t.timestamps end end end
Ok — great — you'll want to spend a fair amount of time with your own app to fill out as many attributes for your models as you can think of (you can easily add more later, so don't worry if you miss something). You'll want to make sure you have setup PostgreSQL on your local machine (← steps 9–11, click here if you are a Windows user), then setup a super user for your database if you haven't yet:
~/apps/OurAgendaApp $ createuser --login --createdb OurAgendaApp #say yes to being a super user if prompted
Now you are ready to run your migrations:
~/apps/OurAgendaApp $ rake db:create ~/apps/OurAgendaApp $ rake db:migrate
FINALLY, INSERT YOUR MODEL ASSOCIATIONS AND TEST THEM
From our list above, open up your five model files (in app/models) and paste in the associations we wrote down:
class User < ActiveRecord::Base has_and_belongs_to_many :meetings has_many :action_items has_many :agenda_items has_many :comments end class Meeting < ActiveRecord::Base has_and_belongs_to_many :users has_many :agenda_items has_many :action_items end class AgendaItem < ActiveRecord::Base belongs_to :user belongs_to :meeting has_many :comments, :as => :commentable end class ActionItem < ActiveRecord::Base belongs_to :user belongs_to :meeting has_many :comments, :as => :commentable end class Comment < ActiveRecord::Base belongs_to :commentable, :polymorphic => true belongs_to :user end
Awesome. Now, in order to secure passwords for our users, making use of that password_digest attribute we added to our user model, we have to add a couple things to our user.rb file to make use of Rails' has_secure_passwordmethod:
class User < ActiveRecord::Base has_and_belongs_to_many :meetings has_many :action_items has_many :agenda_items has_many :comments has_secure_password before_validation :secure_password_digest
private # if password_digest is blank, create a random password. def secure_password_digest if password_digest.blank? self.password = SecureRandom.urlsafe_base64(16) end end end
And we need to add this line to our Gemfile (located in the root dir of your Rails app). You will see this line at approx line 36 and can uncomment it:
gem 'bcrypt-ruby', '~> 3.0.0'
Now that your models are setup, let's go ahead and fire up a console and test them out. This will teach you a lot about how Ruby on Rails works.
~/apps/OurAgendaApp $ bundle install ~/apps/OurAgendaApp $ rails c > User.create! first_name: "Jack", email: "firstname.lastname@example.org", password: "1234", password_confirmation: "1234" > user = User.first > user.meetings.create! name: "test meeting" > meeting = Meeting.first > meeting.agenda_items.create! title: "test agenda item", user_id: user.id > meeting.action_items.create! title: "test action item", user_id: user.id > ActionItem.first.comments.create! body: "Hello World"
…you get the idea. Keep playing around in the console, creating and calling objects, etc… to get a feel for how models and associations work in practice.
Once you are done, “exit” out and go create a new repository in your Github.com account (set one up if you don't have one yet) and call it OurAgendaApp (you do not need to initialize it with a README). Then, assuming you have Git installed and your public SSH key added to Github:
~/apps/OurAgendaApp $ git init ~/apps/OurAgendaApp $ git add . ~/apps/OurAgendaApp $ git commit -m "Initial Commit" ~/apps/OurAgendaApp $ git remote add origin email@example.com:YOUR_GITHUB_USERNAME/OurAgendaApp.git ~/apps/OurAgendaApp $ git push origin master
And there you go, you are on your way. Hopefully you have been repeating these steps with your own app idea? A great place to ask questions if you get stuck is Stack Overflow, or feel free to contact me below.
- — -
In the next post in this series we will learn how to setup this Rails app for test-driven and behavior-driven development. Previous post: How to work with a designer to efficiently build mockups for your app.