So far you've seen how to associate records with one another using foreign keys in a database. Just as we can use ActiveRecord to read, change, update, and delete data from our database, we can use ActiveRecord relationship methods to associate records with one another using Ruby code.
This lesson assumes you have gone through - Rails API: Single Resource with the following:
If you are behind, or don't have correct code, please speak with the instructor immediately so you don't fall further behind.
has_many
and belongs_to
to other macros, like attr_accessor
.has_many
and belongs_to
.We've got a single resource and all of its components (routes, controller, model, migration) for each domain we're working in. Let's go in and create a second resource for each.
In rails-api-library-demo,
you've seen a books
resource created.
In order to create a pairing author
resouce, we'll need to repeat what was
done in the last talk. However, since we've seen this already, we're going to
use a generator that creates more than one piece at a time, and modify it
accordingly.
If we open a browser and hit /authors
we get back: No route matches [GET] \"/authors\"
, which makes sense. We haven't done anything with authors yet.
In order to generate the code we wrote by hand for patients
we can use the
following (shortcut) command:
bin/rails generate scaffold author given_name:string family_name:string
Now let's examine each of the files it created!
~/wdi/training/rails-api-library-demo (tutorial)$ bin/rails generate scaffold
author given_name:string family_name:string
Running via Spring preloader in process 17246
Expected string default value for '--serializer'; got true (boolean)
invoke active_record
create db/migrate/20170419183303_create_authors.rb
create app/models/author.rb
invoke rspec
create spec/models/author_spec.rb
invoke resource_route
route resources :authors
invoke serializer
create app/serializers/author_serializer.rb
invoke scaffold_controller
create app/controllers/authors_controller.rb
invoke rspec
create spec/controllers/authors_controller_spec.rb
create spec/routing/authors_routing_spec.rb
invoke rspec
create spec/requests/authors_spec.rb
route resources :authors
This has just added resources :authors
to our config/routes.rb
file. Let's
add a few modifiers here since we won't need the new
or edit
routes.
Rails.application.routes.draw do
- resources :authors
+ resources :authors, except: [:new, :edit]
create app/controllers/authors_controller.rb
WOAHHHHHH! You mean, that little rails generate
command wrote all of this for
us!??! Somebody get these guys a raise!
Let's walk through the controller
file though and make sure it looks right.
Ok, it looks pretty similar to our PatientsController
, and everything seems
in order. Now, let's make sure it works by testing each action using curl
.
WARNING:
We must be mindful of how much code scaffold
creates, there are many
instances that you may not WANT all actions. How would you modify this file if
you didn't want anyone to be able to create
doctors? Is this the only place
we would need to modify our code? If we delete a create
action, we must also
add it to the except [:new, :edit, :create]
list!
create app/models/author.rb
...not much new here. Author
inherits from ApplicationRecord
which gives us
all the methods we need in the controller (like .new
, .all
, etc.)
create db/migrate/20170419183303_create_authors.rb
This file sets up our migration using the command-line arguments we passed
with bin/rails generate scaffold
command. Since we haven't migrated yet,
we can still modify this file to make some values required. In order to perform
validation before we save an Author to the database, we can alter our migration,
and add the flag null: false
. This tells SQL to prevent records from being
saved that don't have these values present.
class CreateAuthors < ActiveRecord::Migration[5.0]
def change
create_table :authors do |t|
- t.string :given_name
+ t.string :given_name, null: false
- t.string :family_name
+ t.string :family_name, null: false
t.timestamps
end
end
end
create app/serializers/author_serializer.rb
A serializer is a file that allows us to customize the output that rails sends as JSON from our server. By default it looks like this:
class AuthorSerializer < ActiveModel::Serializer
attributes :id, :given_name, :family_name
end
What would happen if we took :given_name
out?
Are there any differences between the JSON that gets sent for an author
, and
what gets sent for book
?
What are created_at
and updated_at
? Let's also add a serializer for
book
while we're at it:
bin/rails generate serializer book
Doctors have given_name
, family_name
, specialty
, and gender
.
Recipes have name
and directions
.
has_many
Often, the resources of our application will have relationships with each other. In our three domains, authors have many books, doctors have many patients, and recipes have many ingredients.
Versus having ownership information writen in two different tables (i.e., doctors' info saved to their own table as well as rewritten in the patients' table), we want to make sure we set up a foreign key association between the two.
How can we reflect this in Rails?
Simple. We begin by applying the has_many
macro to the parent resource models.
has_many
BooksI'll apply the has_many
macro to the Author model.
Once doing this, the has_many
macro provides us with many useful getters and
setters:
Author#books
Author#books<<
Author#books.delete
Author#books.destroy
Author#books=
Author#book_ids
Author#book_ids=
Author#books.clear
Author#books.empty?
Author#books.size
Author#books.find
Author#books.exists?
Author#books.build
Author#books.create
Author#books.create!
belongs_to
To complete this model relationship in Rails, the other side of the
relationship must use the belongs_to
macro.
belongs_to
AuthorWatch as I add this macro to the Book model. Take note of singular vs. plural
conventions for both belongs_to
and has_many
.
has_many
Patients, Patient belongs_to
DoctorLet's add has_many
and belongs_to
macros where appropriate for our doctors
to have many patients and our patients to belong to a doctor.
has_many
Ingredients, Ingredient belongs_to
RecipeGo ahead and set up recipes to have many ingredients, and ingredients to belong to a recipe.
We've almost finished with our relationships. We need one last thing - a foreign key reference column on our books, patients, and ingredients tables. This will allow us to reference the respective author, doctor, and recipe each instance belongs to by ID.
To update our books
migration, we have a couple of options:
We'll be going with the latter. Why? Remember that migrations occur in the order of their timestamps. If we go in and modify our books migration (which, in theory, has an earlier timestamp than the authors migration), and make a reference to the authors table before it exists, our migration will fail.
Watch as I generate this migration change with:
bin/rails generate migration AddAuthorToBooks author:references
Let's play with our results in bin/rails console
and assign Books to Authors
using Book.author = Author.find(id)
.
Together, let's run a migration to add a doctor
column with the appropriate
reference to your patients
table.
Your turn! Run a migration to add a recipe
column with the appropriate
reference to your ingredients
table.