How to scaffold models from an ERD in Rails
04 April 2021
One of the features I love the most about Rails is the ability to quickly scaffold my models into an instant out-of-the-box CRUD interface. The way I would go about this procedure is first to create a barebones Entity Relationship Diagram. The following image depicts one of the diagrams I might create:
This is a slice of a schema belonging to a tailor-made CRM project. And it’s my own brand of ERD diagram. One that helps me to generate the proper model scaffoldings. I’m not interested in specifying all the possible relationships between tables. Only the ones that could or will have an impact on my Rails code. So for example, a product_category
can have zero or many products
in it, but since I’m not specifying that association on the code then I’m not interested in making that distinction in my diagram either. We’ll go over the numbered references in a bit.
I.- The first couple of questions that pop in my head after completing the diagram are: how do I translate this ERD into a set of rails generate
commands? For which entities I need to generate a scaffold and for which entities I need to generate just a model?
The answer I came up with so far is the following: You only rails g model
instead of a full scaffold
in the following cases:
A.- For models that are actually cross-reference or junction entities as in the case of product_variation_options
.
This first case should be pretty self-explanatory. Let’s dive a bit into the schema to understand it. Let’s use a t-shirt as an example of a product
. A t-shirt has_one
product_variation
. A product_variation
is a collection of product_attribute_options
each of which belongs to a product_attribute
. A few examples of t-shirt product_attributes
are: size (and the product_attribute_options
of size could be something like: small, medium, large) and color (red, blue, black, white). So a product_variation
would be a collection of product_attribute_options
: Red large t-shirts. So in the ProductVariation
model we could have something like:
class ProductVariation < ApplicationRecord
belongs_to :account
has_many :product_attribute_options, through: :product_variation_options
end
We need product_variation_options
as a junction or cross-reference entity in order to associate each variation with a number of product_attribute_options
. One may ask what’s the point of having a product_variation
table to begin with. Why can’t we simply assert that a product
has_many
product_attribute_options
. Well, on one hand imagine a product has lots of attributes attached to it. Then you would need to set all the options when creating it. If we bundle them then it’s a matter of selecting the variation (i.e: Red large V neck). And on the other hand, it’s a handy way of retrieving data from different products that may have the same set of product_attribute_options
. Of course you can always get that same data by adding several options to a search filter, but I think it’s cleaner to have a separate variation entity.
In any case, and to come back to the topic at hand, junction or cross-reference entitities like the ones we would put in a through:
association like the one above do not need a scaffold. (The reason why I’m using has_many :through
instead of has_and_belongs_to_many
comes from here).
B.- For models that will become nested attributes
of parent models.
A nested attribute is an attribute that you don’t picture yourself filling in an independent view. For example: I can imagine that when I go and create a product_attribute
such as size or color then I would add the product_attribute_option
on the same page. It wouldn’t make much sense to create a product_attribute
and then go on to a different page where I will create a product_attribute_option
like large and associate it in a form field to the Size product_attribute
and then hit the create button and repeat that process for all my options. So a product_attribute_option
is a perfect candidate for a nested attribute
and as that, it requires only to generate a model instead of a full scaffold.
II.- The second question one should have is: what attributes do I add into each scaffold or model?
The answer to that question is: all the ones that you have put inside the entity boxes.
The full list of generate commands will look like this:
rails g scaffold ProductCategory name:string description:string account:belongs_to
rails g scaffold ProductAttribute name:string description:string account:belongs_to
rails g model ProductAttributeOption name:string description:string product_attribute:belongs_to account:belongs_to
rails g scaffold ProductVariation name:string account:belongs_to
rails g model ProductVariationOption product_attribute_option:references product_variation:references account:belongs_to
rails g scaffold Product product_type:string name:string reference:string description:string price:decimal units_per_package:integer product_category:belongs_to product_variation:references account:belongs_to
Notice that all the foreign keys are added in the commands through a belongs_to
or a references
association. As far as I know the end result will be the same, which is: they will both add a foreign_key
constraint in the table. So for example the Product
scaffold command will result in the following ActiveRecord::Migration
:
class CreateProducts < ActiveRecord::Migration[6.1]
def change
create_table :products do |t|
t.string :product_type
t.string :name
t.string :reference
t.string :description
t.decimal :price
t.integer :units_per_package
t.belongs_to :product_category, null: false, foreign_key: true
t.references :product_variation, null: false, foreign_key: true
t.belongs_to :account, null: false, foreign_key: true
t.timestamps
end
end
end
And will generate the following model:
class Product < ApplicationRecord
belongs_to :product_category
belongs_to :product_variation
belongs_to :account
end
Notice that in the migration it respects the preferred semantic flavor (it puts t.belongs_to
or t.references
depending on what we put in the generate command). Yet in the ApplicationRecord
it translates the t.references
into a belongs_to
association.
III.- The third question one should have is: how do you come up with the correct order of commands?
The order of commands simply reflects the order of dependencies. And by this I mean: I can’t scaffold the products
before scaffolding the product_variations
or the product_categories
because I need to set an association (a foreign key to be more precise) within the products
table that will reference those two other tables. And the same with the rest of the commands. I first execute the ones that do not depend on any table and I continue from then on.
IV.- The fourth question may or may not arise depending on your Rails expertise. It’s a question I do not have anymore but confused me quite a bit a year ago. And it’s the issue of why I was seeing in some Rails projects a has_one
association in a given model instead of a belongs_to
?
The answer is that has_one
and belongs_to
are the two halves of a one-to-one association between two ApplicationRecords
. The half where you put the foreign key referencing the other is where you use belongs_to
and the half where you show the reverse association you would use has_one
. A related question that could pop up is: where do I put the foreign key on this one-to-one case? I found a Toy Story themed example somewhere on stackoverflow that I deem perfect: Andy has_one
Woody, but Woody belongs_to
Andy. And the foreign key goes into Woody because flipping Woody we’ll find a label with the name ‘Andy’ on it. Well, it was enlightening for me. But I agree it may not work for everyone.
belongs_to
of course is used not only for one-to-one associations but also as the half that holds the foreign key of a one-to-many relationship. The above Product
model code shows exactly that use case. A product
has one product_variation
(and in the products
table is where you will have the foreign key to the proper product_variation
row). But we don’t use has_one
because that would mean there’s no foreign key linking a product
to a product_variation
(a product_variation
that could be present in many products
).
V.- Fifth issue. Now that we generated the scaffolds and the models. What do we need to change in our Rails code?
And here’s where we will turn our attention to the numbered references in the ERD diagram. The association arrows listed in the diagram are not exhaustive because I want each association vector to correspond with a change in the Rails code that will not be generated automatically with the generate
commands. Let’s go over those references now:
1.- Here I highlighted that a product
must have a product_category
. This does not translate to a change in the code but is something I want to be aware of. Do I really want this association to be mandatory? I add it to the diagram not because I have to change something in the code but because I may change my mind and then I will need to change something in the code.
2.- Here I highlighted that a product
may or may not have a product_variation
. By default, all belongs_to
will be required. But here we’ve decided that a Product
may not have a product_variation
after all if the product_type
is set to simple
instead of variable
. That’s why we need to change the following line from the CreateProducts
migration:
...
t.references :product_variation, null: true, foreign_key: true
...
And we have to add an optional flag into our scaffolded model:
...
class Product < ApplicationRecord
...
belongs_to :product_variation, optional: true
...
end
3.- A product_attribute
can have one or many product_attribute_options
. So we need to add that has_many
relationship into the product_attribute
ApplicationRecord
. And we want to destroy
the options if the attribute gets deleted so we don’t end up with orphaned attribute options.
...
has_many :product_attribute_options, dependent: :destroy
...
Notice we don’t include here some missing changes related to the nature of product_attribute_options
as a nested attribute
of product_attribute
(we’ll tackle that in a different post).
4.- A product_variation
can have one or many product_variation_options
. Then we go ahead and add that to product_variation
ApplicationRecord
(we already showed this code snippet above):
...
class ProductVariation < ApplicationRecord
belongs_to :account
has_many :product_attribute_options, through: :product_variation_options
end
...
So in a nutshell, after executing the generate commands we’ve taken care of two main issues: changing the flag of any optional belongs_to
associations (and we must do this before running the migrations) and also add any necessary has_many
associations to the ApplicationRecords
(including its dependent:
option if required).