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.
Related
I want to make sure table join with joining table has strong one-to-one relationship. Following is example
Run generator
bundle exec rails g model Page name:string content:text
bundle exec rails g model PageAssociation left_page_id:integer right_page_id:integer
After running these generator, I have to change my page model following
class Page < ApplicationRecord
has_one :left_page_association, :foreign_key => :left_page_id,
:class_name => 'PageAssociation'
has_one :right_page_association, :foreign_key => :right_page_id,
:class_name => 'PageAssociation'
has_one :left_page, :through => :left_page_association,
:source => :right_page
has_one :right_page, :through => :right_page_association,
:source => :left_page
end
In my association table model page_association
class PageAssociation < ApplicationRecord
belongs_to :left_page, class_name: 'Page'
belongs_to :right_page, class_name: 'Page'
end
Now using rails c I can do following
page_one = Page.create(name: 'first page', content: 'first page content')
page_two = Page.create(name: 'second page', content: 'second page content')
PageAssociation.create(left_page: page_one, right_page: page_two)
page_one.left_page
page_two.right_page
All working fine and it is returning pages. but I can do also this
page_three = Page.create(name: 'third page', content: 'third page content')
PageAssociation.create(left_page: page_one, right_page: page_three)
Of course it still shows on relationship due to has_one but it is creating another relationship. So I want following
Only one association can be created and it should not allow another one
What is best way to do this as we have million of records
Is it one to use page.left_page? or is there any other optimised way to do same thing.
Should I add indexing using following migration lines. Does it effect performance.
add_foreign_key :page_associations, :pages, column: :left_page_id
add_foreign_key :page_associations, :pages, column: :right_page_id
I changed migration to make column value unique, so it now give error when I create another PageAssociate with same page.id but is it right way?
t.integer :left_page_id, null: false, index: { unique: true }
t.integer :right_page_id, null: false, index: { unique: true }
what problem I am solving
So I have books table where I have multiple books, but some books borrow pages from one another. So suppose I have book-A and book-B so I want to show relationship of book-a to book-b. If they are related then I will create another relationship of book-a page-10 linked to book-b page 20, so when I click on book-a to synchronise, it will bring all changes of book-b page 20 in my book-a.
So above is my first step to connected two books. I understand solving it using self join and keys is best but I can't do as we have huge number of records so that is not possible. So I have used above method to do it. But I did add unique constraints on db and validations in model to solve it. but I feel it is still not good way of doing it.
later one I will make following
main-book , main-book-page, secondary-book-page
This will allow me to bound later pages each other. The name book and pages are fictions, actually entities are different.
What you want here is really just a normal many to many table setup:
class Book < ApplicationRecord
has_many :book_pages
has_many :pages, through: :book_pages
end
class Page < ApplicationRecord
has_many :book_pages
has_many :books, through: :book_pages
end
class BookPage < ApplicationRecord
belongs_to :book
belongs_to :page
validates_uniqueness_of :book_id, scope: :page_id
end
Uniqueness in this case can be guarenteed by adding a unique index:
add_index :book_pages, [:book_id, :page_id]
Why?
M2M join table setups where you have two foreign keys that are assigned arbitrarily are not a very good design and don't work well with ActiveRecord since you can't define assocations with an OR clause.
Each association can only have a single foreign key. This means that you can't treat it as a homogenius collection and cannot eager load it.
That means you need to write crappy joins like this instead of being able to work with a proper assocation:
Book.joins(
"LEFT JOIN pages_assocations pa ON pa.left_page_id = books.id OR pa.left_page_id = books.id"
)
And you also have to write steaming piles when creating indirect assocations.
While the table setup with a single row per book/page combo may seem to require more rows on the onset its also much more flexible as you can map out the assocations between books by subqueries, lateral joins or grouping and counting the number of matches.
class Book < ApplicationRecord
has_many :book_pages
has_many :pages, through: :book_pages
def books_with_pages_in_common
Book.where(
id: BookPage.select(:book_id)
.where(page_id: pages)
)
end
end
Let's say, on a rails app (with postgresql), I have this database with a polymorphic association between work_steps and sub_assemblies / parts:
Part.rb
belongs_to :sub_assembly, optional: true
has_many :work_steps, as: :component
belongs_to :site
belongs_to :car
Sub_assembly.rb
has_many :work_steps, as: :component
has_many :parts
work_step.rb
belongs_to :component, polymorphic: true
Now, I want to query my database: I want a list of all the work_steps filtered with a specific array of sites and a specific array of cars.
For instance, all the work_steps linked to parts that belongs to Site A and Car X & Z.
Because of the polymorphic association with the sub_assemblies and this same table which is linked to parts table, I cannot figure out how to do it.
At the end, I need an ActiveRecord Relation. How would you do it simply? I tried a lot of things but without success.
Any one to help me?
Thank you in advance.
Okay, so work_step is what you want to be able to be used by multiple models.
So inside the migration for CreateWorkSteps:
t.references :component, polymorphic: true
This basically does
t.integer :component_id # Refers to id (of sub-assembly or part)
t.string :component_type # Refers to type of object (sub-assembly or part)
add_index :work_steps, [:component_type, component_id]
Now in your model:
work_step.rb
belongs_to :component, polymorphic: true
sub_assembly.rb
has_many :work_steps, as: :component
has_many :parts
part.rb
has_many :work_steps, as: :component
belongs_to :site
belongs_to :car
belongs_to :sub_assembly, optional: true
Okay, so you wish to query your database and obtain
work_steps
linked to parts
belongs to site A and Car X and Z
So, here the list of steps you should do in ActiveRecord (I'm not going to give a single-query answer as that's too much effort. Also, the step order is in logical order, but remember that ActiveRecord queries use lazy evaluation and SQL queries are evaluated in a certain order.)
#Join parts w/ site and cars
Use #Where siteName is in [A], carName is in [X, Z]
Now, you LEFT join with sub_assembly (So even if the part has no sub_assembly_id, it isn't removed).
Now this is the annoying step. For your first join, you'll want to join parts_id with component_id AND component_type == "Part" (Remember to prefix your tables when joining, e.g work_steps.component_id and do this from the beginning.)
For your second join, you'll want to join sub_assembly_id with component_id AND component_type == "Sub_assembly"
Now you have a HUGE ActiveRecord object, select only what makes a work_step.
Good luck! I won't hold your hand with a pre-written answer, through...because I'm too lazy to boot into Rails, make migrations, make models, seed the models, then write the query myself. ;)
Oh ya, and I ended up giving a single query answer anyway.
I have a Ruby on rails application where users can add one or more software.
These users can then subscribe (paid mode).
I would like to be able to display all paid user software only.
I tried several things and still can not find the solution.
for example:
Software.includes(:users).where(user: {subscribed: true})
EDIT:
Model USER:
class User < ActiveRecord::Base
has_many :softwares
end
Model Softwares:
class Software < ActiveRecord::Base
belongs_to :user
end
JoinTable
class CreateJoinTableUsersSoftwares < ActiveRecord::Migration[5.2]
def change
create_join_table :users, :softwares do |t|
t.index [:user_id, :software_id]
t.index [:software_id, :user_id]
end
end
end
ERROR with :
Software.includes(:users).where(user: {subscribed: true})
ActiveRecord::ConfigurationError (Can't join 'Software' to association named 'users'; perhaps you misspelled it?)
I think your error is here:
Software.includes(:users).where(user: {subscribed: true})
The includes should reflect the association, in this case a singular user. This is what's causing the error you're seeing.
Also, it's a common gotcha, but the association within the where clause needs to use the table name.
Try this:
Software.includes(:user).where(users: { subscribed: true })
This assumes you'll be using the info on the user elsewhere, i.e. in your view. If you don't need to access the record, rather just check the user for the query, you can switch includes to joins to improve the query's efficiency:
Software.joins(:user).where(users: { subscribed: true })
It's a separate topic, but there's a good read on it here.
I have a parent ProductCategory and child Product. For example:
ProductCategory --- Product
Drill --- DeWalt DWD 112
--- Black & Decker 5 C
Bicycle --- Motobecane Turino ELITE Disc Brake
--- Shimano Aluminum
For a given ProductCategory, there are a set of attributes that all Products should be comparable with each other on (i.e., have data on). However, this set of attributes is likely to vary between ProductCategories
For example, for the ProductCategory of Drill, the attributes might be Voltage, Amps, Corded vs Cordless. Every Product Drill needs to have such information. However, for the ProductCategory of Bicycle, the attributes should be Size, Road vs Mountain, and every Product bicycle needs to have this information. (Sorry I don't know anything about either drills or bikes... why I picked this was stupid)
I'm trying to design the DB such that for a given Product, the attributes are something that I can easily search. For example, ideally I can run this command:
drills = Product.where(product_category_id:1)
drills.where("voltage >= ?", 5)
-> returns the individual drills, which may include DeWalt but not Black & Decker
This seems to present an interesting trade-off... because then I'd have to have Product have columns for every attribute for every ProductCategory, even those that aren't relevant to it. For example:
# Product columns
:voltage, :integer #for Drill
:amps, :integer #for Drill
:corded, :boolean #for Drill
:size, :integer #for Bicycle
:mountain, :boolean #for Bicycle
...
This doesn't seem sustainable... you can see very quickly that for just a few ProductCategories there will soon be an infinite number of Product columns!
At the other end of the spectrum, I thought about having defining attributes required of each Product in the parent ProductCategory, and then requesting these attributes/storing them on Product as a stringified data:
# ProductCategory has a column...
:required_attributes, :text
ProductCategory.where(name:"Drill").first.required_attributes
-> "voltage,amps,corded"
ProductCategory.where(name:"Bicycle").first.required_attributes
-> "size,mountain"
# Product has a column...
:attribute_data, :text
Product.where(name:"DeWalt").first.attribute_data
-> "{'voltage':5,'amps':5,'corded':5}"
With the design above, I could create a front end that enforced that, upon Product creation, one has to provide information for each required_attributes after it's been split based on commas. But of course, this makes searching much less efficient, at least I THINK it does... so this is my question. How can I efficiently search stringified data? If I'm searching for all Drills with at least 5 volts, so complete the below.
drills = ProductCategory.where(name:"Drill")
drills.where("attribute_data ...")
The simplest solution is just to use a JSON or HSTORE datatype column on products to store the specifications.
But if you want to have a bit more control and validations per specification you can use a design with a join table:
class Product
has_many :specs
has_many :definitions, through: :specs
end
# This is the "normalized" table that defines an specification
# for example "Size". This just holds the name for example and the acceptable values.
class Definition
has_many :specs
has_many :products, through: :specs
end
# this contains the actual specs of a product (the value)
# specs.payload is a JSON column
class Spec
belongs_to :definition
belongs_to :product
delegate :name, to: :definition
def value
payload[:value]
end
def value=(val)
payload[:value] = val
end
end
One of the classic problems with using a design like this would be that the specs table would have to store the value as a text (or varchar) column and deal with the issues of type casting. But most modern DB's support dynamic column types like HSTORE or JSON that you can use to store the actual value.
The downside is that you have to use a special SQL syntax when querying:
Spec.where("payload->>'value' = ?", "foo")
This is a sort of normalized variation on what is called the Entity–attribute–value model which can be an anti-pattern but is often the only good solution to dynamic attributes in a relational database.
See:
ActiveRecord and PostgreSQL
ActiveRecord::ActsAs - Multi Table Inheritance for AR
Another way to approach the problem which avoids the issue of EAV tables is to use multi-table inheritance. This example uses ActiveRecord::ActsAs.
class Product < ActiveRecord::Base
actable
belongs_to :brand
validates_numericality_of :universal_product_code, length: 12
validates_presence_of :model_name, :price
end
class Bicycle < ActiveRecord::Base
acts_as :product
validates_numericality_of :gears
validates_presence_of :size
end
class PowerTool < ActiveRecord::Base
acts_as :product
validates_numericality_of :voltage, :amps
validates_presence_of :voltage, :amps
end
This would store the base information on a products table:
change_table :products do |t|
t.decimal :price
t.sting :model_name
t.integer :universal_product_code, length: 12
t.integer :actable_id
t.string :actable_type
end
And it uses a polymorphic association to store the subtypes of products on more specific tables:
create_table :bicycles do |t|
t.integer :gears
t.integer :size
end
create_table :power_tools do |t|
t.boolean :corded
t.integer :amps
t.integer :size
end
The advantage here is that you have a defined schema instead of a bunch of loose attributes.
The drawback is if you are designing a generic web shop then a fixed schema will not cut it.
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