I have a model Place.
For instance, place can be a city, a district, a region, or a country, but I think one model will be enough, because there is no big difference between city and region for my purposes.
And places can belong to each other, e.g. a region can have many cities, a country can have many regions etc.
But my problem is that it's kind of historical project and a city can belong to many regions. For example, from 1800 to 1900 a city belonged to one historical region (that doesn't exist now) and from 1900 till now - to another. And it's important to store those regions as different places despite of they can have similar geographical borders but only names are different.
I guess it's many-to-many, but maybe someone can give a better idea?
And if it's many-to-many, how can I get a chain of parent places in one query to make just simple string like "Place, Parent place 1, Parent place 2, Parent place 3", e.g. "San Francisco, California, USA"?
Here is my code:
create_table :places do |t|
t.string :name
t.timestamps null: false
end
create_table :place_relations, id: false do |t|
t.integer :sub_place_id
t.integer :parent_place_id
t.timestamps null: false
end
class Place < ActiveRecord::Base
has_and_belongs_to_many :parent_places,
class_name: "Place",
join_table: "place_relations",
association_foreign_key: "parent_place_id"
has_and_belongs_to_many :sub_places,
class_name: "Place",
join_table: "place_relations",
association_foreign_key: 'sub_place_id'
end
Please, don't hesitate to give me some ideas about it!
This is the first solution that popped in my mind, and there may be many other ways to do it, but I believe this may arguable be the cleanest.
You've got the right general idea, but all you need is a slight modification to the join table. Essentially you'll use has_many... through relationships instead, so that you can append some kind of time frame discriminator.
In my examples, I'm using a datetime field to indicate from what point the association is relevant. In combination with a default scope to order the results by the time discriminator (called effective_from in my examples), you can easily select the "current" parents and children of a place without additional effort, or select historical data using a single date comparison in the where clause. Note that you do not need to handle the time frame discrimination as I did, it is merely to demonstrate the concept. Modify as needed.
class PlaceRelation < ActiveRecord::Base
default_scope { order "effective_from DESC" }
belongs_to :parent, class_name: "Place"
belongs_to :child, class_name: "Place"
end
class Place < ActiveRecord::Base
has_many :parent_places, class_name: "PlaceRelation", foreign_key: "child_id"
has_many :child_places, class_name: "PlaceRelation", foreign_key: "parent_id"
has_many :parents, through: :parent_places, source: :parent
has_many :children, through: :child_places, source: :child
end
and the migration for the place_relations table should look like this:
class CreatePlaceRelations < ActiveRecord::Migration
def change
create_table :place_relations do |t|
t.integer :parent_id
t.integer :child_id
t.datetime :effective_from
t.timestamps
end
end
end
So if we create a couple of "top level" country-places:
country1 = Place.create(name: "USA")
country2 = Place.create(name: "New California Republic")
and a state place
state = Place.create("California")
and a city
city = Place.create("San Francisco")
and finally tie them all together:
state.parent_places.create(parent_id: country1.id, effective_from: DateTime.now - 1.year)
state.parent_places.create(parent_id: country2.id, effective_from: DateTime.now)
city.parent_places(parent_id: state.id, effective_from: DateTime.now)
Then you would have a city ("San Francisco") which belongs to the state "California", which historically belonged to the country "USA", and later "New California Republic".
Additionally, if you would like to build a string containing the place's name and all its "parents", you could do it "recursively" like so:
def full_name(name_string = [])
name_string << self.name
parent = self.parents.first
if parent.present?
return parent.full_name name_string
else
return name_string.join(", ")
end
end
Which, in the case of our city "San Francisco", should result in "San Francisco, California, New California Republic", given the ordering of the effective_from field.
This makes a association of direct many to many relation with another model without intervening that model . But you can use more advanced stuff if you want like Polymorphic Association .
For More Information please visit Rails Guide : Polymorphic Association
Related
I am looking to create an association where a model can be created and owned by three different entities but also references the other two entities.
For example, I have a performance model where three different types of models can create a performance: venue, artist, band. However, the performance also needs to reference the other two e.g if a venue creates a performance, it needs to list an artist or a band that will be performing. And if an artist creates a performance, then the artist needs to put a venue where he/she will be performing.
So I am starting with something like this:
class CreatePerformances < ActiveRecord::Migration[6.0]
def change
create_table :performances, id: :uuid do |t|
t.belongs_to :venue, index: true
t.belongs_to :artist, index: true
t.belongs_to :band, index: true
t.timestamps
end
end
end
However, if a venue owner creates a performance and has two separate bands performing then I would need to have an array of bands in the band_id column. But when I do that (t.belongs_to :band, type: :uuid, array: true, default: [], index: true) and add a band to the band_id array and then do band.performances I get: ActiveRecord::StatementInvalid (PG::InvalidTextRepresentation: ERROR: malformed array literal:
Can I make an association column an array and still be able to use the Rails association features or is that not possible or even bad practice, and if so how?
Also, I am using postgresql and if you have a more elegant ways of doing the above that would also be appreciated.
I guess it's better to use a has_many relationship.
If a performance can have many bands playing then it does not "belongs to" a band, it "has many" bands.
So you may use a "has and belongs to many" or a "has many :through" relationship between performances and bands. Check the differences here https://guides.rubyonrails.org/association_basics.html#choosing-between-has-many-through-and-has-and-belongs-to-many
The easiest of the two to configure is HABTM:
class Band
has_and_belongs_to_many :performances
class Performance
has_and_belongs_to_many :bands
You need a table, so add a miration that does this:
create_table :bands_performances, id: false do |t|
t.references :band, index: true
t.references :performance, index: true
end
https://guides.rubyonrails.org/association_basics.html#creating-join-tables-for-has-and-belongs-to-many-associations
Check the guide, if you need extra fields you may need a join model and use has_many :through. You know the context better than anyone.
I have the following database schema and models, which constitute a self-referential association where a Phrase has many translations of other Phrases, through the Translation association:
Schema:
ActiveRecord::Schema.define(version: 20151003213732) do
enable_extension "plpgsql"
create_table "phrases", force: :cascade do |t|
t.integer "language"
t.string "text"
end
create_table "translations", force: :cascade do |t|
t.integer "source_id"
t.integer "destination_id"
end
end
Phrase:
class Phrase < ActiveRecord::Base
has_many :translations,
class_name: "Translation",
foreign_key: "source_id"
has_many :destination_phrases,
through: :translations
has_many :inverse_translations,
class_name: "Translation",
foreign_key: "destination_id"
has_many :source_phrases,
through: :inverse_translations
enum language: [:eng, :spa, ...]
end
Translation:
class Translation < ActiveRecord::Base
belongs_to :source_phrase,
class_name: "Phrase",
foreign_key: "source_id"
belongs_to :destination_phrase,
class_name: "Phrase",
foreign_key: "destination_id"
end
Now, I want to run queries based on source language and destination language. For example, I want to find English phrases and their respective Spanish translations. Currently I am querying phrases based on source language, but then I have to filter out the results using the select method on the destination language. What I have looks like the following:
#translations = Translation.includes(:source_phrase, :destination_phrase)
.where(phrases: {language: #source_language})
# Select only destination phrases where the language matches
#translations = #translations.select {
|t| t.destination_phrase.language == #destination_language
}
I want to eliminate the select call because this should definitely be possible in ActiveRecord. The select would be replaced by an additional parameter in the model's where query, but I cant figure out how to specify it.
It should look something like this:
#translations =
Translation.includes(:source_phrase, :destination_phrase)
.where([source_phrase: {language: #source_language},
destination_phrase: {language: #destination_language}])
However, ActiveRecord thinks (rightfully) that source_phrase and destination_phrase in the where clause are table names. So the table name still has to be phrases, but when it is I can't specify the join condition for both joins, just the first.
How can I specify 2 separate join conditions on a self-referential association which both access the same attribute on the same model (language on the Phrase model)?
My Phrase model turned out to be defined incorrectly, which was the first problem. I've revised it as so:
Phrase:
class Phrase < ActiveRecord::Base
has_many :translations,
class_name: "Translation",
foreign_key: "source_id"
has_many :source_phrases, # this changed
through: :translations
has_many :inverse_translations,
class_name: "Translation",
foreign_key: "destination_id"
has_many :destination_phrases, # this changed
through: :inverse_translations
enum language: [:eng, :spa, ...]
end
Translation has remained the same:
class Translation < ActiveRecord::Base
belongs_to :source_phrase,
class_name: "Phrase",
foreign_key: "source_id"
belongs_to :destination_phrase,
class_name: "Phrase",
foreign_key: "destination_id"
end
Now I am able to perform either of the following queries to get the correct results:
Translation
.includes(:source_phrase => :source_phrases, :destination_phrase => :destination_phrases)
.where(source_phrases_phrases: {language: 0}, destination_phrases_phrases: {language: 2})
or
Translation
.joins(:source_phrase => :source_phrases, :destination_phrase => :destination_phrases)
.where(source_phrases_phrases: {language: 0}, destination_phrases_phrases: {language: 2})
.distinct
The answer was in specifying the joined association (in this case source_phrases and destination_phrases in Phrase), which could then be used as different table names in the where clause.
I should note that I use source_phrases_phrases and destination_phrases_phrases in the where clause because Rails seems to expect the table name (phrases) to be appended to the associations (source_phrases and destination_phrases). It makes for a few ugly queries, but perhaps I could name my associations in the Phrase model better...
It also seems that from the EXPLAIN output (way too long to put here), the joins version is about 25% faster with 6,000 phrases and 12,000 translations. It also exponentially gets faster the larger those tables grow.
Lesson learned? Self-referential associations are a pain. I certainly hope that this helps someone in the future.
I am new with Rails and I would need some advice :) I have this:
class Club < ActiveRecord::Base
has_many :players, :through => :club_players
has_many :club_players
end
class Player < ActiveRecord::Base
has_many :clubs, :through => :club_players
has_many :club_players
end
class ClubPlayer < ActiveRecord::Base
belongs_to :club
end
create_table "players", force: true do |t|
t.string "name"
t.string "age"
t.string "nationality"
t.string "sex"
end
Now I would like to model search engine where users can search players by age and have that defined like this in drop down:
Team Seniors
Team Kids
Age [14-18]
Age [19-25]
Age [26-30]
Age [31-35]
Age [36-40]
Retired
Or by nationality:
American
Indian
French
Mixed
How can my search engine look like if user clicks on Age [19-25]? Where would be a good idea to define what certain string in drop down represent? The same thing with nationality. For example I need to define somewhere what category A Team Seniors represent. I would define that team as a team which has more than 50% of players older than 25 years. So, I need somehow to define categories which are defined from some columns of Player.
What would be the best practice in doing this? Thanks!
I would create Tags for each of these categories. Then you could apply as many/little of the tags you want to apply to them. Or you create some look up tables/lists for the different sections like nationality.
For age, you probably need birthday and you could then create an age method to give you their current age.
I have a parent class called Place.
# Parent class for Country, City and District
class Place < ActiveRecord::Base
end
class Country < Place
has_many :cities, foreign_key: "parent_id"
has_many :districts, through: :cities
end
class City < Place
belongs_to :country, foreign_key: "parent_id"
has_many :districts, foreign_key: "parent_id"
end
class District < Place
belongs_to :city, foreign_key: 'parent_id'
has_one :country, through: :city
end
The schema:
create_table "places", force: true do |t|
t.string "name"
t.string "type"
t.integer "parent_id"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "places", ["parent_id"], name: "index_places_on_parent_id"
add_index "places", ["type"], name: "index_places_on_type"
The following works as expected:
#country.cities # => Returns all of the cities that belong to this country
#city.districts # => Returns all of the districts that belong to this city
But this does not work as I thought it would:
#country.districts # => Does not return all of the districts belonging to cities in this country
Can anybody explain how I should approach has many through with STIs?
Update
Here's the output SQL query from #country.districts
SELECT "places".* FROM "places" INNER JOIN "places" "cities_districts_join" ON "places"."parent_id" = "cities_districts_join"."id" WHERE "places"."type" IN ('City') AND "places"."type" IN ('District') AND "cities_districts_join"."parent_id" = ? [["parent_id", 1]]
I think the problem is that it's using the same join table for both relations, but I'm not sure if there's a "Rails way" to change the name of the joins table (elegantly)
This is a challenging case for ActiveRecord. It needs to infer that columns in the self-join needed to find districts are STI instances. Apparently it's not smart enough to get this right. Since the only table is places, it's not much of a surprise that it generates this query:
SELECT "places".* FROM "places"
INNER JOIN "places" "cities_districts_join"
ON "places"."parent_id" = "cities_districts_join"."id"
WHERE "places"."type" IN ('City') <<<<< ERROR HERE
AND "places"."type" IN ('District')
AND "cities_districts_join"."parent_id" = ?
As you can see the type check must fail since one string can't be both City and District. All would work if the first clause in the WHERE were instead
WHERE "cities_districts_join"."type" IN ('City')
I tried several options on the relations (thought :class_name might do it), but no joy.
You can work around this limitation with SQL. Delete the has_many ... through in the Country class and replace with
def districts
District.find_by_sql(['SELECT * from places AS city
INNER JOIN places AS district
ON district.parent_id = city.id
WHERE city.parent_id = ?', id])
end
Or maybe someone else will see a more elegant way. If not, you might consider posting this as an issue in Rails development. It's an interesting case.
I think you need to change the inheritance of your models.
class Country < Place
class City < Country
class District < City
And then remove the
has_one :country through: :city
line.
Scroll down to find info about STI
http://api.rubyonrails.org/classes/ActiveRecord/Base.html
Rails app:
A user has_many positions.
Each position has one company (company name and company id) per the following schema:
create_table "positions", :force => true do |t|
t.integer "user_id"
...
t.string "company"
t.integer "company_id"
end
I would like users to be able to "follow" as many individual companies as they would like (i.e. a user can follow many different companies a company can be followed by many different users). It would seem that this calls for a has_and_belongs_to_many relationship between users and positions, but I want users to be able to follow the company attribute of a position row and not the position itself.
Should I create a new "following" table altogether that would pull companies from the positions table to be matched to user_id's? Or is there a way I can set up a has_many :through relationship and map user_id's to company_id's?
Thank you!
What I think you could have
A User table:
integer User_Id
....
A Company Table:
string company
integer company_id
...
A Positions table:
integer user_id foreign_key -> User table
integer company_id foreign_key -> company table
A Following table (If the user can follow any comapny regarding of whether he has a position in it):
integer user_id foreign_key -> User table
integer company_id foreign_key -> company table
OR if the user can only follow a company that he has position in then you can add a new column to position table. This would be a boolean flag telling if the user if 'following' the company identified by the position. Alternatively the Following table can also map user to position in this case.
I broadly agree with MickJ, although having created the Company and User models/tables (which obviously have an id column in each) I'd do it as:
create_table "companies" do |t|
t.string "name"
...
end
create_table "positions" do |t|
t.references "user"
t.references "company"
...
end
create_table "followings" do |t|
t.references "user"
t.references "company"
...
end
Models:
class User
has_many :positions
has_many :followings
end
class Company
has_many :positions
has_many :followings
end
class Position
belongs_to :user
belongs_to :company
end
class Following
belongs_to :user
belongs_to :company
end
You could reference the company from the position by doing:
position = Position.first
puts position.company.name
or by user with something like
user = User.first
user.positions.each do |position|
puts position.company.name
end
-- EDIT1:
To extract the company name from positions into a separate table you'd be best off writing a little rake task - something like:
Position.all.each do |position|
company = Company.find_or_initialize_by_name(position.company_name)
position.company_id = company.id
position.save
end
Then you might want to write a migration to remove the company name column from the positions table ... just to keep things tidy.