I am a Sr. Software Developer at Oracle Cloud. The opinions expressed here are my own and not necessarily those of my employer.
Global ID and has_and_belongs_to_many
Recently I had to design a feature where user could save specific reports with preset filter options to make it easier to run them. The challenge is that reports can be ran across different types of records so modeling the relationships was interesting.
- Separate relationships in the same class
- Single Table Inheritance
- Polymorphic HABTM relationship with a mapping table
- GlobaldID
- Useful links
Separate relationships in the same class
Here is the first design I went with in my Rails 4.2 application.
When users logs into dashboard they will see their saved reports and be able to run them with one click. Depending on report_type it will call separate classes with the actual queries for aggregating the data. Each report can gather data for one or more accounts or articles. And I set inverse_of: :nil
because from Article or Account model I do not need to know which reports it’s included in.
The problem is we are storing all relationship / logic in the same class / table even though account report_type will never have :articles
. So we have to create conditional validation. Plus there could be other filter params specific to only some of the reports. Overall it just feels like putting too many things in the same place.
Single Table Inheritance
Next I created separate classes.
Here the logic is spread across appropriate classes so it’s much more modular. The challenge is from the UI you now can no longer do Report.create(..)
.
Polymorphic HABTM relationship with a mapping table
I previously wrote about creating polymorphic has_and_belongs_to_many relationships.
Here you have to build custom validation that if report_type == :account
then ReportMap can only belong_to Account record_type. Plus you must have relationships from the Account or Article side. And you first need to create Report and then ReportMap records (which are needed to actually generate data).
GlobaldID
The solution I settled on is using GlobalID where we have URIs like this gid://MyApp/Some::Model/id
. We can store array of these on only one side of the relationship. It’s similar to regular polymorphic relationship but you can have many of them.
get_records
method will return the actual objects based on their GlobalID using GlobalID::Locator.locate
and records_type
validation uses model_name
to make sure they are all the same and of correct type.
You also could have a report that gathers data for both accounts and articles. For that you need to setup different when case
in the validator method.
As you can see all of these solutions are much more complex that usual belongs_to and has_many. You have to create appropriate validations and test your code carefully.
Useful links
- https://github.com/rails/globalid
- http://stefan.haflidason.com/simpler-polymorphic-selects-in-rails-4-with-global-id/
- http://stackoverflow.com/questions/34084140/using-global-id-to-specify-an-object-in-a-polymorphic-association
- http://dev.mikamai.com/post/96343027199/rails-42-new-gems-active-job-and-global-id