-
-
Notifications
You must be signed in to change notification settings - Fork 15
Inherited Tables
Tired of
polymorphic
or the infamoustype
column. Well, NERVER MORE!!!
This is one great feature from PostgreSQL, the ability to have a table with the most generic data, and then many other tables with the information necessary only for that specific type of the main one. Rails do allow us to do that, using the type
attribute, but its biggest problem is that columns from different types ended up getting mixed together. PostgreSQL Docs
This will allow you to work with inherited models as they are separated tables but yet sharing methods, scopes, validations, and all the other features from all the super models.
CAUTION PostgreSQL has some caveats while using this resource. They will be addressed in later versions of this GEM.
First, you have to create a base table. Then, create many tables as wanted, specifying the :inherited
key.
create_table "activities" do |t|
t.string "title"
end
create_table "activity_books", inherits: :activities do |t|
t.belongs_to "author_id"
t.datetime "published_at"
end
create_table "activity_posts", inherits: :activities do |t|
t.belongs_to "post_id"
t.datetime "published_at"
end
In other to have your models working correctly and take full advantage of this feature, the same way the tables are inherited, the models need to be inherited as well.
# models/activity.rb
class Activity < ApplicationRecord
end
# models/activity_book.rb
class ActivityBook < Activity
end
# models/activity_post.rb
class ActivityPost < Activity
end
Since the data that indicates the record type is a table name, this process relies on rails common translation from class name to table name. But this is tricky, because activity_posts
can either be ActivityPost
or Activity::Post
(or even another model using self.table_name = "activity_posts"
). This GEM tries its best to translate the table name to a model name, chicking all the possibilities, so you might not face issues. But, you can always help it, and make the process more accurate or even faster.
Please, check the Configuration Page in order to see the options to improve this operation.
The most important setting is the irregular_models
, which can both help to describe the correct relationship between a table name and a model name and improve the performance by avoiding the default behavior of searching for the model based on the table name. Although the table name once associated with a model name is cached, be aware of this for setting irregular names.
Torque::PostgreSQL.configure do |c|
c.irregular_models = {
'my_awesome_table_name' => 'SimpleModel'
}
end
Any already loaded record can be casted to its original model using cast_record
method. Be careful that this method relies on a primary_key
to be correctly casted.
ActivityPost.create(title: 'Post 1')
Activity.first # #<Activity id: 1, title: "Post 1" ...
Activity.first.cast_record # #<ActivityPost id: 1, title: "Post 1" ...
You can also inform the relation to automatically cast all the returned records to their original model using cast_records
while querying.
Activity.create(title: 'Activity 1')
ActivityPost.create(title: 'Post 1')
ActivityBook.create(title: 'Book 1')
list = Activity.all.cast_records.load.to_a
list.first # #<Activity id: 1, title: "Activity 1" ...
list.second # #<ActivityPost id: 2, title: "Post 1" ...
list.third # #<ActivityBook id: 3, title: "Book 1" ...
The cast_records
provides other filters like specifying which classes should be casted and if they should be filtered while querying.
# No filter applied
list = Activity.all.cast_records(ActivityPost).load.to_a
list.first # #<Activity id: 1, title: "Activity 1" ...
list.second # #<ActivityPost id: 2, title: "Post 1" ...
list.third # #<Activity id: 3, title: "Book 1" ...
# With filter applied
list = Activity.all.cast_records(ActivityPost, filter: true).load.to_a
list.first # #<Activity id: 1, title: "Activity 1" ...
list.second # #<ActivityPost id: 2, title: "Post 1" ...
list.third # nil
With this feature, another method was introduced. The itself_only
only allows queries on the base table to not include any inherited record. It uses the FROM ONLY
SQL clause, so it's very performative.
list = Activity.itself_only.load.to_a
list.first # #<Activity id: 1, title: "Activity 1" ...
list.second # nil
list.third # nil
In order to perform any query with correct performance and maintainability, this feature uses Auxiliary Statements and Dynamic Attributes.
DEPRECATED: This is being replaced by a cast operation that translates the tableoid into its table name (AKA regclass), as in
"activities"."tableoid"::regclass
. This improvement removes the necessity of an auxiliary statement and an extra join in queries. The support for getting the actual table name of a record will change in next versions.
The _record_class
method (which can be renamed using the inheritance.record_class_column_name
setting) is used both as an Auxiliary Statement and a Dynamic Attribute to get the type of the records. While it's a great example of these provided features, you can always take advantage of this to get the type of the record.
list = Activity.all.with(:_record_class).load.to_a
list.first._record_class # "activities"
list.second._record_class # "activity_posts"
list.third._record_class # "activity_books"
Can't find what you're looking for? Add an issue to the issue tracker.