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
- 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
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).
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
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.