Given tables with integer and uuid primary keys what is the best way to integrate a polymorphic join (has_many)? For example:
class Interest < ActiveRecord::Base
# id is an integer
has_many :likes, as: :likeable
end
class Post < ActiveRecord::Base
# id is a UUID
has_many :likes, as: :likeable
end
class User < ActiveRecord::Base
has_many :likes
has_many :posts, through: :likes, source: :likeable, source_type: "Post"
has_many :interests, through: :likes, source: :likeable, source_type: "Interest"
end
class Like < ActiveRecord::Base
# likeable_id and likeable_type are strings
belongs_to :likeable, polymorphic: true
belongs_to :user
end
Many queries work:
interest.likes
post.likes
user.likes
However:
user.interests
Gives:
PG::UndefinedFunction: ERROR: operator does not exist: integer = character varying
LINE 1: ...interests" INNER JOIN "likes" ON "interests"."id" = "likes"....
^
HINT: No operator matches the given name and argument type(s). You might need to add explicit type casts.
: SELECT "interests".* FROM "interests" INNER JOIN "likes" ON "interests"."id" = "likes"."likeable_id" WHERE "likes"."user_id" = $1 AND "likes"."likeable_type" = $2
What's the best way to include ensure the proper casting happens?
This is an old question, but here's my recommendation.
This is more of an architecture problem. Don't combine UUID ids and integer ids, it get's messy real fast. If you can, migrate the integer IDs to UUID or revert the uuids to integer ids.
My experience has been that the best solution is probably to make use of the rather nice Friendly ID gem: https://github.com/norman/friendly_id
In the off case this is broken in the future, it is basically just a slug generation/managemnet tool, the slug would use this kind of route path: posts/this-is-a-potential-slug instead of posts/1, but nothing prevents you from using posts/<UUID here> or posts/<alphanumeric string here>.
Typically if you are using UUIDs it's because you don't want to show the sequential integers. Friendly ID works well to avoid that issue.
There's no means to specify the necessary cast using Rails. Instead, add a generated column with the cast, and declare an extra belongs_to association to use it. For example, with this in a migration:
add_column :interests, :_id_s, 'TEXT GENERATED ALWAYS AS (id::text) STORED'
add_index :interests, :_id_s
and this in your models:
class Like
belongs_to :_likeable_cast, polymorphic: true, primary_key: :_id_s, foreign_key: :likeable_id, foreign_type: :likeable_type
class User
has_many :interests, through: :likes, source: :_likeable_cast, source_type: "Interest"
then user.interests joins through the alternative association, i.e. using the generated column with the cast.
I suggest using a column type of text rather than varchar for the likeable_id column, to avoid unnecessary conversions during the join and ensure the index is used.
Can you describe your likes table? I suppose that it contains
user_id as integer,
likeable_id as integer,
likeable_type as integer
any third-part fields
So, technically you can not create the same polymorphic association with uuid string and id as integer in scope of two fields likeable_id and likeable_type.
As solution - you can simply add id as primary key to posts table instead of uuid. In case if you maybe do not want to show id of post in URL, or for another security reasons - you can still use uuid as before.
You might be able to define your own method to retrieve likes in your Interest model.
def likes
Like.where("likeable_type = ? AND likeable_id = ?::text", self.class.name, id)
end
The problem with this solution is that you're not defining the association, so something like 'has_many through' won't work, you'd have to define those methods/queries yourself as well.
Have you considered something like playing around with typecasting the foreign- or primary-key in the association macro? E.g. has_many :likes, foreign_key: "id::UUID" or something similar.
Tested on Rails 6.1.4
Having a likeable_id as string works well and rails takes care of the casting of IDs.
Here is an example of my code
Migration for adding polymorphic "owner" to timeline_event model
class AddOwnerToTimelineEvent < ActiveRecord::Migration[6.1]
def change
add_column :timeline_events, :owner_type, :string, null: true
add_column :timeline_events, :owner_id, :string, null: true
end
end
Polymorphic model
class TimelineEvent < ApplicationRecord
belongs_to :owner, polymorphic: true
end
Now we have 2 owner, Contact which has id as Bigint and Company which has id as uuid, you could see in the SQL that rails has already casted them to strings
contact.timeline_events
TimelineEvent Load (5.8ms) SELECT "timeline_events"."id", "timeline_events"."at_time",
"timeline_events"."created_at", "timeline_events"."updated_at",
"timeline_events"."owner_type", "timeline_events"."owner_id" FROM
"timeline_events" WHERE "timeline_events"."owner_id" = $1 AND
"timeline_events"."owner_type" = $2 [["owner_id", "1"],
["owner_type", "Contact"]]
company.timeline_events
TimelineEvent Load (1.3ms) SELECT "timeline_events"."id", "timeline_events"."action",
"timeline_events"."at_time", "timeline_events"."created_at",
"timeline_events"."updated_at", "timeline_events"."owner_type",
"timeline_events"."owner_id" FROM "timeline_events" WHERE
"timeline_events"."owner_id" = $1 AND "timeline_events"."owner_type" =
$2 [["owner_id", "0b967b7c-8b15-4560-adac-17a6970a4274"],
["owner_type", "Company"]]
There is a caveat though when you are loading timeline_events for a particular owner type and rails cannot do the type casting for you
have to do the casting yourself. for e.g. loading timelines where owner is a Company
TimelineEvent.where(
"(owner_type = 'Company' AND uuid(owner_id) in (:companies))",
companies: Company.select(:id)
)
I'm not good with ActiveRecord, and this is definitely not the answer you're looking for, but if you need a temporary *ugly workaround till you can find a solution, you could override the getter :
class User
def interests
self.likes.select{|like| like.likeable._type == 'Interest'}.map(&:likeable)
end
end
*Very ugly cause it will load all the user likes and then sort them
EDIT I found this interesting article :
self.likes.inject([]) do |result, like|
result << like.likeable if like.likeable._type = 'Interest'
result
end
Related
On my postgres db, I have a primary key using a UUID. Sample setup below
class Visit
# primary key id: uuid
has_many :connections, as: :connectable
has_many :payments, through: :connections
end
class Connection #(polymorphic class joining visit and payment)
# columns connectable_type(string), connectable_id(string)
belongs_to :payments
belongs_to :connectable, polymorphic: true
end
class Payment
# primary key id: uuid
has_many :connections
end
When I try to fetch all visits with payments, I got an error:
Visit.joins(:payments)
# => operator does not exist: character varying = uuid`
Basically this requires that I explicitly cast the visit.id to varchar, which I could easily do if my join statements were a string, by:
connections.connectable_id = visits.id::varchar
however I'm using Arel for composability.
Could anyone guide as to how I can typecast this directly with Arel, so I could easily do something like:
join(connections_table).on(connections_table[:connectable_id].eq(cast_to_string(visits_table[:id])))
# where connections_table and visits_table are Arel tables
While playing around with this, I found out about Arel NamedFunction which basically is a way to wrap your [custom] SQL functions in Arel. In this case, I ended up with:
casted_visits_primary_key = Arel::Nodes::NamedFunction.new("CAST", [ visits_table[:id].as("VARCHAR") ])
And then I was able to do:
join(connections_table).on(connections_table[:connectable_id].eq(casted_visits_primary_key))
And that basically solved my problem!
I'm using Rails 5.0.0.1 ATM and i've come across issue with ActiveRecord relations when optimizing count of my DB requests.
Right now I have:
Model A (let's say 'Orders'), Model B ('OrderDispatches'), Model C ('Person') and Model D ('PersonVersion').
Table 'people' consists only of 'id' and 'hidden' flag, rest of the people data sits in 'person_versions' ('name', 'surname' and some things that can change over time, like scientific title).
Every Order has 'receiving_person_id' as for the person which recorded order in DB and every OrderDispatch has 'dispatching_person_id' for the person, which delivered order. Also Order and OrderDispatch have creation time.
One Order has many dispatches.
The straightforward relations thus is:
has_many :receiving_person, through: :person, foreign_key: "receiving_person_id", class_name: 'PersonVersion'
But when I list my order with according dispatches I have to deal with N+1 situation, because to find accurate (according to the creation date of Order/OrderDispatch) PersonVersion for every receiving_person_id and dispatching_person_id I'm making another requests.
SELECT *
FROM person_versions
WHERE effective_date_from <= ? AND person_id = ?
ORDER BY effective_date_from
LIMIT 1
First '?' is Order/OrderDispatch creation date and second '?' is receiving/ordering person id.
Using this query I'm getting accurate person data for the time of Order/OrderDispatch creation.
It's fairly easy to write query with subquery (or subqueries, as Order comes with OrderDispatches on one list) in raw SQL, but I have no idea how to do that using ActiveRecord.
I tried to write custom has_one relation as this is as far as I've come:
has_one :receiving_person. -> {
where("person_versions.id = (
SELECT id
FROM person_versions sub_pv1
WHERE sub_pv1.date_from <= orders.receive_date
AND sub_pv1.person_id = orders.receiving_person_id
LIMIT 1)")},
through: :person, class_name: "PersonVersion", primary_key: "person_id", source: :person_version
It works if I use this only for receiving or dispatching person. When I try to eager_load this for joined orders and order_dispatches tables then one of 'person_versions' has to be aliased and in my custom where clause it isn't (no way to predict if it's gonna be aliased or not, it's used both ways).
Different aproach would be this:
has_one :receiving_person, -> {
where(:id => PersonVersion.where("
person_versions.date_from <= orders.receive_date
AND person_versions.person_id = orders.receiving_person_id").order(date_from: :desc).limit(1)},
through: :person, class_name: "PersonVersion", primary_key: "person_id", source: :person_version
Raw 'person_versions' in where is OK, because it's in subquery and using symbol ':id' makes raw SQL get correct aliases for person_versions table joined to orders and order_dispatches, but I get 'IN' instead of 'eqauls' for person_versions.id xx subquery and MySQL can't do LIMIT in subqueries which are used with IN/ANY/ALL statements, so I just get random person_version.
So TL;DR I need to transform 'has_many through' to 'has_one' using custom 'where' clause which looks for newest record amongst those which date is lower than originating record creation.
EDIT: Another TL;DR for simplification
def receiving_person
receiving_person_id = self.receiving_person_id
receive_date = self.receive_date
PersonVersion.where(:person_id => receiving_person_id, :hidden => 0).where.has{date_from <= receive_date}.order(date_from: :desc, id: :desc).first
end
I need this method converted to 'has_one' relation so that i could 'eager_load' this.
I would change your schema as it's conflicting with your business domain, restructuring it would alleviate your n+1 problem
class Person < ActiveRecord::Base
has_many :versions, class_name: PersonVersion, dependent: :destroy
has_one :current_version, class_name: PersonVersion
end
class PersonVersion < ActiveRecord::Base
belongs_to :person, inverse_of: :versions,
default_scope ->{
order("person_versions.id desc")
}
end
class Order < ActiveRecord::Base
has_many :order_dispatches, dependent: :destroy
end
class OrderDispatch < ActiveRecord::Base
belongs_to :order
belongs_to :receiving_person_version, class_name: PersonVersion
has_one :receiving_person, through: :receiving_person_version
end
Context
In the context of a Ruby on Rails application, in a school's project.
Let's consider the context of a team-based game, with many characters to choose from. I want to represent affinities between two characters in different context, which means whether two characters are being teamed-up or are facing each other or even when one is present in the game while the other is missing.
I would then have tables that looks something like this in my database
Characters
Ally-Relation
Enemy-Relation
PlayingSingle-Relation
Each of these <name>-Relation tables represents a many-to-many relation between Characters, with an additional score that represents the strongness of the relation
Of course, relations between character are subject to changes. We might decide for any reason that a relation has become irrelevant, or another relation that we didn't thought of before just appeared.
In terms of display, we want to look for both the best and worst other characters in a specific relation.
Question
I came up with something like this.
class Relation < ActiveRecord::Base
scope :best, ->(character_id) {
Character.find(where(character_left: character_id).order("score desc").limit(5).pluck(:character_right))
}
end
Where character_left and character_right are the two characters to be considered for in the relation and the score is the strenght of the bond.
However, when fetching data, my teacher thinks it would be best to have scopes in the Characters model to find both the best and worst other character in a specific relation. This, because the teammate that is doing, say, the HTML code don't give a damn about the structure of the Relations when he wants to display characters. He told me about using has_and_belongs_to_many and he sketched me some code he would expects that looks something like Character.best(:relation) to fetch the data.
While I think what I did is better (obviously :) ). Having the scopes that will fetch Characters from within the Relation models, as they subject to appear and disappear keeps the request relation specifics. This prevents us from modifying the Characters model every time we fumble with the Relations.
Having somethings that looks like Relation.best(:hero) seems cleaner to me.
What do you think about it ? What are good practices around this very specific situation. Are there any right way to apply and use modular many-to-many relation s in a Ruby on Rails application ?
Your on the right track with a score column and using that to order the relations. However you need to account for the fact that a character can be in either column in the join model.
class Character
has_many :relationships_as_left, foreign_key: 'left_id'
has_many :relationships_as_right, foreign_key: 'right_id'
end
# renamed to not get it mixed up with ActiveRecord::Relation
class Relationship
belongs_to :left, class_name: 'Character'
belongs_to :right, class_name: 'Character'
end
You want to make sure to setup a unique index and the correct foreign keys:
class CreateRelationships < ActiveRecord::Migration
def change
create_table :relationships do |t|
t.references :left, index: true, foreign_key: false
t.references :right, index: true, foreign_key: false
t.integer :score, index: true
t.timestamps null: false
end
add_foreign_key :relationships, :characters, column: :left_id
add_foreign_key :relationships, :characters, column: :right_id
add_index :relationships, [:left_id, :right_id], unique: true
end
end
Querying this table is kind of tricky since Character can be referenced in relationships.left_id or relationships.right_id.
class Relationship < ActiveRecord::Base
belongs_to :left, class_name: 'Character'
belongs_to :right, class_name: 'Character'
def self.by_character(c)
sql = "relationships.left_id = :id OR relationships.right_id = :id"
where( sql, id: c.id )
end
def self.between(c1, c2)
where(left_id: [c1,c2]).merge(where(right_id: [c1,c2]))
end
def other_character(c)
raise ArgumentError unless c == left || c == right
c == left ? right : left
end
end
The between method requires a little explaination:
where(left_id: [c1,c2]).merge(where(right_id: [c1,c2]))
This generates the following query:
SELECT
"relationships".* FROM "relationships"
WHERE
"relationships"."left_id" IN (1, 2)
AND
"relationships"."right_id" IN (1, 2)
Also both you and your professor are wrong - a scope on Character will not work since scopes are class level, what you want is to check the relations on an instance.
class Character
def worst_enemies(limit = 10)
relations = Relationship.joins(:left, :right)
.by_character(self)
.order('relationship.score ASC')
.limit(limit)
relations.map do |r|
r.other_character(self)
end
end
end
You could possibly do this more elegantly with a subquery.
Using Rails 3.2, what's wrong with this code?
#reviews = #user.reviews.includes(:user, :reviewable)
.where('reviewable_type = ? AND reviewable.shop_type = ?', 'Shop', 'cafe')
It raises this error:
Can not eagerly load the polymorphic association :reviewable
If I remove the reviewable.shop_type = ? condition, it works.
How can I filter based on the reviewable_type and reviewable.shop_type (which is actually shop.shop_type)?
My guess is that your models look like this:
class User < ActiveRecord::Base
has_many :reviews
end
class Review < ActiveRecord::Base
belongs_to :user
belongs_to :reviewable, polymorphic: true
end
class Shop < ActiveRecord::Base
has_many :reviews, as: :reviewable
end
You are unable to do that query for several reasons.
ActiveRecord is unable to build the join without additional information.
There is no table called reviewable
To solve this issue, you need to explicitly define the relationship between Review and Shop.
class Review < ActiveRecord::Base
belongs_to :user
belongs_to :reviewable, polymorphic: true
# For Rails < 4
belongs_to :shop, foreign_key: 'reviewable_id', conditions: "reviews.reviewable_type = 'Shop'"
# For Rails >= 4
belongs_to :shop, -> { where(reviews: {reviewable_type: 'Shop'}) }, foreign_key: 'reviewable_id'
# Ensure review.shop returns nil unless review.reviewable_type == "Shop"
def shop
return unless reviewable_type == "Shop"
super
end
end
Then you can query like this:
Review.includes(:shop).where(shops: {shop_type: 'cafe'})
Notice that the table name is shops and not reviewable. There should not be a table called reviewable in the database.
I believe this to be easier and more flexible than explicitly defining the join between Review and Shop since it allows you to eager load in addition to querying by related fields.
The reason that this is necessary is that ActiveRecord cannot build a join based on reviewable alone, since multiple tables represent the other end of the join, and SQL, as far as I know, does not allow you join a table named by the value stored in a column. By defining the extra relationship belongs_to :shop, you are giving ActiveRecord the information it needs to complete the join.
If you get an ActiveRecord::EagerLoadPolymorphicError, it's because includes decided to call eager_load when polymorphic associations are only supported by preload. It's in the documentation here: http://api.rubyonrails.org/v5.1/classes/ActiveRecord/EagerLoadPolymorphicError.html
So always use preload for polymorphic associations. There is one caveat for this: you cannot query the polymorphic assocition in where clauses (which makes sense, since the polymorphic association represents multiple tables.)
This did the work for me
belongs_to :shop, foreign_type: 'Shop', foreign_key: 'reviewable_id'
Not enough reputation to comment to extend the response from Moses Lucas above, I had to make a small tweak to get it to work in Rails 7 as I was receiving the following error:
ArgumentError: Unknown key: :foreign_type. Valid keys are: :class_name, :anonymous_class, :primary_key, :foreign_key, :dependent, :validate, :inverse_of, :strict_loading, :autosave, :required, :touch, :polymorphic, :counter_cache, :optional, :default
Instead of belongs_to :shop, foreign_type: 'Shop', foreign_key: 'reviewable_id'
I went with belongs_to :shop, class_name: 'Shop', foreign_key: 'reviewable_id'
The only difference here is changing foreign_type: to class_name:!
As an addendum the answer at the top, which is excellent, you can also specify :include on the association if for some reason the query you are using is not including the model's table and you are getting undefined table errors.
Like so:
belongs_to :shop,
foreign_key: 'reviewable_id',
conditions: "reviews.reviewable_type = 'Shop'",
include: :reviews
Without the :include option, if you merely access the association review.shop in the example above, you will get an UndefinedTable error ( tested in Rails 3, not 4 ) because the association will do SELECT FROM shops WHERE shop.id = 1 AND ( reviews.review_type = 'Shop' ).
The :include option will force a JOIN instead. :)
#reviews = #user.reviews.includes(:user, :reviewable)
.where('reviewable_type = ? AND reviewable.shop_type = ?', 'Shop', 'cafe').references(:reviewable)
When you are using SQL fragments with WHERE, references is necessary to join your association.
I have 3 models: Question, Option, Rule
Question has_many options;
Option needs a foreign key for question_id
Rule table consists of 3 foreign_keys:
2 columns/references to question_ids -> foreign keys named as 'assumption_question_id' and 'consequent_question_id'
1 column/reference to option_id -> foreign key named as option_id or condition_id
Associations for Rule:
Question has_many rules; and
Option has_one rule
I want to understand how to write up migrations for this, and how that associates to the 'has_many'/'belongs_to' statements I write up in my model, and the ':foreign_key' option I can include in my model.
I had this for my Option migration, but I'm not sure how the "add_index" statement works in terms of foreign keys, and how I can use it for my Rule migration: (my Question and Options models have appropriate has_many and belongs_to statements - and work fine)
class CreateOptions < ActiveRecord::Migration
def change
create_table :options do |t|
t.integer :question_id
t.string :name
t.integer :order
t.timestamps
end
add_index :options, :question_id
end
end
Thank you for the help!
Note: I have found this way to solve the problem.Kindness from China.
If you have RailsAdmin with you,you may notice that you can see all rules of one question as long as one field of both question fields(assumption_question_id,consequent_question_id) equals to id of the question.
I have done detailed test on this and found out that Rails always generates a condition "question_id = [current_id]" which make to_sql outputs
SELECT `rules`.* FROM `rules` WHERE `rules`.`question_id` = 170
And the reason that the following model
class Question < ActiveRecord::Base
has_many :options
# Notice ↓
has_many :rules, ->(question) { where("assumption_question_id = ? OR consequent_question_id = ?", question.id, question.id) }, class_name: 'Rule'
# Notice ↑
end
makes Question.take.rules.to_sql be like this
SELECT `rules`.* FROM `rules` WHERE `rules`.`question_id` = 170 AND (assumption_question_id = 170 OR consequent_question_id = 170)
Is that we have not yet get ride of the annoy question_id so no matter how we describe or condition properly, our condition follows that "AND".
Then,we need to get ride of it.How?
Click here and you will know how,Find sector 8.1,and you can see
Article.where(id: 10, trashed: false).unscope(where: :id)
# SELECT "articles".* FROM "articles" WHERE trashed = 0
Then lets do it:
class Question < ActiveRecord::Base
has_many :options
# Notice ↓
has_many :rules, ->(question) { unscope(where: :question_id).where("assumption_question_id = ? OR consequent_question_id = ?", question.id, question.id) }, class_name: 'Rule'
# Notice ↑
end
class Rule < ActiveRecord::Base
belongs_to :option
belongs_to :assumption_question, class_name: "Question", foreign_key: :assumption_question_id, inverse_of: :assumption_rules
belongs_to :consequent_question, class_name: "Question", foreign_key: :consequent_question_id, inverse_of: :consequent_rules
end
class Option < ActiveRecord::Base
belongs_to :question
has_one :rule
end
All done.
Finally
This is my first answer here at stackoverflow,and this method is never found anywhere else.
Thanks for reading.
add_index adds an index to column specified, nothing more.
Rails does not provide native support in migrations for managing foreign keys. Such functionality is included in gems like foreigner. Read the documentation that gem to learn how it's used.
As for the associations, just add the columns you mentioned in your Question to each table (the migration you provided looks fine; maybe it's missing a :rule_id?)
Then specify the associations in your models. To get you started
class Question < ActiveRecord::Base
has_many :options
has_many :assumption_rules, class_name: "Rule"
has_many :consequent_rules, class_name: "Rule"
end
class Rule < ActiveRecord::Base
belongs_to :option
belongs_to :assumption_question, class_name: "Question", foreign_key: :assumption_question_id, inverse_of: :assumption_rules
belongs_to :consequent_question, class_name: "Question", foreign_key: :consequent_question_id, inverse_of: :consequent_rules
end
class Option < ActiveRecord::Base
belongs_to :question
has_one :rule
end
Note This is just a (untested) start; options may be missing.
I strongly recommend you read
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
http://guides.rubyonrails.org/association_basics.html
Edit: To answer the question in your comment
class Option < ActiveRecord::Base
belongs_to :question
# ...
The belongs_to tells rails that the question_id column in your options table stores an id value for a record in your questions table. Rails guesses the name of the column is question_id based on the :question symbol. You could instruct rails to look at a different column in the options table by specifying an option like foreign_key: :question_reference_identifier if that was the name of the column. (Note your Rule class in my code above uses the foreign_key option in this way).
Your migrations are nothing more than instructions which Rails will read and perform commands on your database based from. Your models' associations (has_many, belongs_to, etc...) inform Rails as to how you would like Active Record to work with your data, providing you with a clear and simple way to interact with your data. Models and migrations never interact with one another; they both independently interact with your database.
You can set a foreign key in your model like this:
class Leaf < ActiveRecord::Base
belongs_to :tree, :foreign_key => "leaf_code"
end
You do not need to specify this in a migration, rails will pull the foreign key from the model class definition.