Single Foregn Key references multiple Tables [Rails6] - ruby-on-rails

I have an application with "Categories" "Players" & "Matches".
I want to create a new Table: "Suggestions", with the params:
name: string,
description: text,
type: integer,
element_id: integer
Users will be able to create "Suggestions" for a Player, Category or a Match. type:integer will indicates what type of suggestion is this
"Category Suggestion" -> 1
"Player Suggestion" -> 2
"Match Suggestion" -> 3
My question is, how do I reference multiple tables with only one foreign key(element_id)? This foreign key can be from "Category", "Player" or "Match", depending the "type" of the Suggestion.
I thought about a solution in the models where I just place the foreign key there, but I'm not sure if this can cause problems in my application.
Also I thought about creating 3 tables CategorySuggestions, PlayersSuggestions and MatchesSuggestions but that would just be overkill and I wouldn't like that so much.

What you are looking for is Polymorphic Associations. You can add something like,
class Suggestion
...
belongs_to :suggestable, polymorphic: true
...
end
In the database, you'll add two columns to the suggestions table.
t.bigint :suggestable_id
t.string :suggestable_type
Then you can simply use suggestion.suggestable which will give you the corresponding object of the correct type, without having to manage any of the types or integers yourself.

Related

Return name in ActiveRecord relation along with foreign key id

I have a Sub-Component model which can belong to other sub-components. My Model looks like this:
class SubComponent < ApplicationRecord
belongs_to :parent, class_name: "SubComponent", foreign_key: "parent_sub_component_id", optional: true
has_many :child_sub_components, class_name: "SubComponent", foreign_key: "parent_sub_component_id"
validates_presence_of :name
end
This model is fairly simple, it has a name field and a parent_sub_component_id which as the name suggests is an id of another SubComponent.
I'd like to generate a query that returns all of the SubComponents (with their id, name, and parent_sub_component_id) but also includes the actual name of it's parent_sub_component.
This seems like it should be pretty simple but for the life of me I can't figure out how to do it. I'd like for this query to be done in the database rather than doing an each loop in Ruby or something like that.
EDIT:
I'd like for the output to look something like this:
#<ActiveRecord::Relation [#<SubComponent id: 1, name: "Parent Sub", parent_sub_component_id: nil, parent_sub_component_name: nil created_at: "2017-07-07 00:29:37", updated_at: "2017-07-07 00:29:37">, #<SubComponent id: 2, name: "Child Sub", parent_sub_component_id: 1, parent_sub_component_name: "Parent Sub" created_at: "2017-07-07 00:29:37", updated_at: "2017-07-07 00:29:37">]>
You can do this efficiently using an each loop if you use includes:
SubComponent.all.includes(:parent).each do |comp|
comp.parent.name # this gives you the name of the parent
end
What includes does is it pre-fetches the specified association. That is, ActiveRecord will query all subcomponents, and then in a single query also pull down all the parents of those subcomponents. When you subsequently access comp.parent in the loop, the associated parent will already be loaded, so this will not result in a so-called N+1 query.
The queries that AR will generate for you automatically will look something like this:
SELECT `subcomponents`.* FROM `subcomponents`
SELECT `subcomponents`.* FROM `subcomponents` WHERE `subcomponents`.`id` IN (1, 3, 9, 14)
If you need to use the name of the parent in a where condition, includes will not work and you will have to use joins instead to actually generate an SQL JOIN.
This is untested, but should get you started in the right direction, you can do this in Arel by doing something like
def self.execute_query
parent_table = Arel::Table.new(:sub_component).alias
child_table = Arel::Table.new(:sub_component)
child_table.join(parent_table, Arel::Nodes::OuterJoin).on(child_table[:parent_sub_component_id].eq(parent_table[:id]).project(child_table[:id], child_table[:name], parent_table[:id], parent_table[:name])
end
This results in a query like
SELECT "sub_component"."id", "sub_component"."name", "sub_component_2"."id", "sub_component_2"."name" FROM "sub_component" LEFT OUTER JOIN "sub_component" "sub_component_2" ON "sub_component"."parent_sub_component_id" = "sub_component_2"."id"
this is just off the top of my head by looking at Rails/Arel and probably needs a some work, but the query looks about what I would expect and this should get you going.

Incorrect pluralizing of column name when creating foreign key

I'm having a strange problem with Rails 4.2.4. I am creating a new table, which references some others, like this:
t.references :local, index: true, foreign_key: true, null: false
t.references :serie, index: true, foreign_key: true, null: false
when I execute the migration, there is an error when creating the foreign key constraint:
PG::UndefinedColumn: ERROR: no existe la columna «series_id» referida en la llave foránea
which is Spanish for
PG::UndefinedColumn: ERROR: column «series_id» referenced in foreign key constraint does not exist
meaning there is no "series_id" column in the created table. Of course, it shouldn't be any column with that name.
The correct column name the generation of FK should be looking for is "serie_id", and it does exist.
Now the strangest thing is that it doesn't fail for :local, for instance. It doesn't look for "locales_id", but "local_id" which is correct, and the corresponding FK is created.
I have custom Spanish inflections, and correct pluralizations are:
local -> locales
serie -> series
however I don't understand why the FK generation seems to be pluralizing in one case and not in the other.
I have found a working solution in this answer, which is declaring specifically the foreign keys, like:
add_foreign_key :turnos_registrados, :series, column: :serie_id
but what I'd like to know is why is this happening.
It seems that Rails does this procedure to get the foreign key column name:
referenced model in migration → (convert symbol to string) → pluralize → singularize
The pluralization is done when finding a table name for the given referenced model, see this line in the source code. Then, the singularization is done when deriving the actual foreign key column from the previously pluralized table name (see the source here).
However, in English, "series" is a collective noun, so the singular and plural forms are the same. Rails handles this correctly by default:
"series".pluralize
# => "series"
"series".singularize
# => "series"
But also note this:
"serie".pluralize
# => "series"
So, the "s" appendix is first added to the serie referenced model name and then the singularization yields the same word - "series". I.e. Rails yield the following for the :serie referenced model from your migration:
:serie → "serie" (conversion to string) → "series" (pluralize) → "series" (singularize)
So this is why Rails tries to look for series_id foreign key column but not for locales_id.
You write that you managed to add the correct key manually using add_foreign_key. I suspect that you will encounter more problems with your setup later as Rails will still try to derive the foreign key as series_id.
Either you'd have to specify the correct (serie_id) foreign_key everywhere in your associations or you should define a custom pluralization rule for your case. The rule should be added as an initializer that contains:
ActiveSupport::Inflector.inflections do |inflect|
inflect.irregular 'serie', 'series'
end
Once you have this, the pluralization will work as expected for your particular case:
"series".singularize
# => "serie"
And with this custom rule I think that even your original migration should work without problems.
Thanks to BoraMa's answer, I went further and found out the "why":
seems that some high-priority Rails default english inflections were getting in the way.
I have an inflection rule
inflect.singular(/((?<![aeiou][rndlj])e|a|i|o|u)s([A-Z]|_|$)/, '\1\2')
which should catch a
"series".singularize
since
2.2.1 :006 > "series".gsub(/((?<![aeiou][rndlj])e|a|i|o|u)s([A-Z]|_|$)/,'\1\2')
=> "serie"
but it wasn't working. Somehow, adding the suggested
inflect.irregular 'serie', 'series'
made it work, but it should work without too. So, I suspected of some pre-defined rule, which can be confirmed (another BoraMa's contribution) in the console because the response to the sentence
ActiveSupport::Inflector.inflections.singulars
will print among others: [/(s)eries$/i, "\1eries"].
In order to get rid of these defaults, adding
inflect.clear
to inflections.rb made it. Now it works with the more general rule I originally had.

Custom filter for grid view not working (two associated models)

I am using wice_grid and have two associated tables: Organization and User (with a 1:many relationship). For a grid view of all users I would like to include a column which states the name of the organization that the user belongs to (a variable in the organization model). To this end, I defined a custom filter, for which the gem provides instructions here.
I defined the following column for the grid view:
g.column name: 'Organization', filter_type: :string, attribute: 'users.organization_id',
custom_filter: Organization.all.map{|pr| [pr.id, pr.organizationname]} do |user|
link_to(user.organization.organizationname, organization_path(user.organization))
end
The error message that this generates (referring to the first line):
WiceGrid: Invalid attribute name users.organization_id. An attribute name must not contain a table name!
But I do feel I've exactly what the example in the instructions also has. Any idea what I'm doing wrong?
Update: If I change the first line to:
g.column name: 'Organization', filter_type: :string, attribute: 'organization_id',
the page renders without error. However, the filter for this column is a drop-box rather then a search field for a string (changing it to filter_type: :string has no effect). In addition, if I try to sort the column, the page gives the error below. Any ideas how to define the column/custom filter?
PG::InvalidTextRepresentation: ERROR: invalid input syntax for integer: "Gutmann-Stroman 95"
LINE 1: ...OM "users" WHERE (( users.organization_id = 'Gutmann-S...
^
: SELECT COUNT(*) FROM "users" WHERE (( users.organization_id = 'Gutmann-Stroman 95'))
organization_id in the user table holds the id number of the organization the user belongs to. "Gutmann-Stroman 95" is the organizationname associated to the id. I'm using friendly_id for friendly url's, which in the url converts the id's in the names; perhaps this has something to do with it?
But I do feel I've exactly what the example in the instructions also has.
Wrong. The example specifies model: 'Project', and you don't. The example uses name of the joined table and you don't. Your code is as far away from the example as possible. Also, where in the documentation do you see filter_type: :string ?
You should have something like
g.column name: 'Organization', model: 'Organization', attribute: 'organizationname' do |user|
link_to(
user.organization.organizationname,
organization_path(user.organization)
)
end
and in initialize_grid you need to include your assocation correctly:
yourgrid = initialize_grid(User,
include: [:organization]
)
Here you can see a working example: http://wicegrid.herokuapp.com/joining_tables

Separate indexes on two columns and unique constraint on the pair in Rails

My app uses a PostgreSQL database. I've got a migration that looks like this:
class CreateTagAssignments < ActiveRecord::Migration
def change
create_table :tag_assignments do |t|
t.integer :tag_id
t.integer :quote_id
t.integer :user_id
t.timestamps
end
add_index :tag_assignments, :tag_id
add_index :tag_assignments, :quote_id
end
end
Records will be quite frequently searched by these two columns so I want to have a separate index for each. But now I'd like to enforce uniqueness of the pair (tag_id, quote_id) on the database level. I tried add_index :tag_assignments, [:tag_id, :quote_id], unique: true but I got the error:
PG::Error: ERROR: could not create unique index "index_tag_assignments_on_tag_id_and_quote_id"
DETAIL: Key (tag_id, quote_id)=(10, 1) is duplicated.
: CREATE UNIQUE INDEX "index_tag_assignments_on_tag_id_and_quote_id" ON "tag_assignments" ("tag_id", "quote_id")
So multiple indexes apparently do the job of a multi-column index? If so, then I could add the constraint with ALTER TABLE ... ADD CONSTRAINT, but how can I do it in ActiveRecord?
edit: manually performing ALTER TABLE ... ADD CONSTRAINT produces the same error.
As Erwin points out, the "Key (tag_id, quote_id)=(10, 1) is duplicated" constraint violation error message tells you that your unique constraint is already violated by your existing data. I infer from what's visible of your model that different users can each introduce a common association between a tag and a quote, so you see duplicates when you try to constrain uniqueness for just the quote_id,tag_id pair. Compound indexes are still useful for index access on leading keys (though slightly less efficiently than a single column index since the compound index will have lower key-density). You could probably get the speed you require along with the appropriate unique constraint with two indexes, a single column index on one of the ids and a compound index on all three ids with the other id as its leading field. If mapping from tag to quote was a more frequent access path than mapping from quote to tag, I would try this:
add_index :tag_assignments, :tag_id
add_index :tag_assignments, [:quote_id,:tag_id,:user_id], unique: true
If you're using Pg >= 9.2, you can take advantage of 9.2's index visibility maps to enable index-only scans of covering indexes. In this case there may be benefit to making the first index above contain all three ids, with tag_id and quote_id leading:
add_index :tag_assignments, [:tag_id,:quote_id,user_id]
It's unclear how user_id constrains your queries, so you may find that you want indexes with its position promoted earlier as well.
So multiple indexes apparently do the job of a multi-column index?
This conclusion is untrue as well as unfounded after what you describe. The error message indicates the opposite. A multicolumn index or a UNIQUE constraint on multiple columns (implementing a multi-column index, too) provide functionality that you cannot get out of multiple single-column indexes.

Modeling many-to-many :through with Mongoid/MongoDB

I'm relatively new to Mongoid/MongoDB and I have a question about how to model a specific many-to-many relationship.
I have a User model and a Project model. Users can belong to many projects, and each project membership includes one role (eg. "administrator", "editor", or "viewer"). If I were using ActiveRecord then I'd set up a many-to-many association between User and Project using has_many :through and then I'd put a field for role in the join table.
What is a good way to model this scenario in MongoDB and how would I declare that model with Mongoid? The example below seems like a good way to model this, but I don't know how to elegantly declare the relational association between User and the embedded ProjectMembership with Mongoid.
Thanks in advance!
db.projects.insert(
{
"name" : "Test project",
"memberships" : [
{
"user_id" : ObjectId("4d730fcfcedc351d67000002"),
"role" : "administrator"
},
{
"role" : "editor",
"user_id" : ObjectId("4d731fe3cedc351fa7000002")
}
]
}
)
db.projects.ensureIndex({"memberships.user_id": 1})
Modeling a good Mongodb schema really depends on how you access your data. In your described case, you will index your memberships.user_id key which seems ok. But your document size will grow as you will add viewers, editors and administrators. Also, your schema will make it difficult to make querys like:
Query projects, where user_id xxx is editor:
Again, you maybe do not need to query projects like this, so your schema looks fine. But if you need to query your projects by user_id AND role, i would recommend you creating a 'project_membership' collection :
db.project_memberships.insert(
{
"project_id" : ObjectId("4d730fcfcedc351d67000032"),
"editors" : [
ObjectId("4d730fcfcedc351d67000002"),
ObjectId("4d730fcfcedc351d67000004")
],
"viewers" : [
ObjectId("4d730fcfcedc351d67000002"),
ObjectId("4d730fcfcedc351d67000004"),
ObjectId("4d730fcfcedc351d67000001"),
ObjectId("4d730fcfcedc351d67000005")
],
"administrator" : [
ObjectId("4d730fcfcedc351d67000011"),
ObjectId("4d730fcfcedc351d67000012")
]
}
)
db.project_memberships.ensureIndex({"editors": 1})
db.project_memberships.ensureIndex({"viewers": 1})
db.project_memberships.ensureIndex({"administrator": 1})
Or even easier... add an index on your project schema:
db.projects.ensureIndex({"memberships.role": 1})

Resources