Generate multiple records based on association - ruby-on-rails

The case is that I have two models with relations many to many. But I want to make a query where I have the number of models multiplied by the relations where I have the same main model with one that has one relation. Here is the case in code:
# models/physician.rb
class Physician < ApplicationRecord
has_many :appointments
has_many :patients, through: :appointments
end
# models/patient.rb
class Patient < ApplicationRecord
has_many :appointments
has_many :physicians, through: :appointments
end
# models/appointment.rb
class Appointment < ApplicationRecord
belongs_to :physician
belongs_to :patient
end
# models/extended_physician.rb
class ExtendedPhysician < ApplicationRecord
self.primary_key = 'id'
has_one :appointment, foreign_key: 'physician_id'
has_one :patient, foreign_key: 'physician_id', through: :appointment
end
# sql view created in scenic for extended_physician model
SELECT "physicians".* FROM "physicians"
LEFT OUTER JOIN "appointments" ON "appointments"."physician_id" = "physicians"."id"
LEFT OUTER JOIN "patients" ON "patients"."id" = "appointments"."patient_id"
# schema
create_table "appointments", force: :cascade do |t|
t.bigint "physician_id"
t.bigint "patient_id"
t.datetime "appointment_date"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["patient_id"], name: "index_appointments_on_patient_id"
t.index ["physician_id"], name: "index_appointments_on_physician_id"
end
create_table "patients", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "physicians", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_view "extended_physicians", sql_definition: <<-SQL
SELECT physicians.id,
physicians.name,
physicians.created_at,
physicians.updated_at
FROM ((physicians
LEFT JOIN appointments ON ((appointments.physician_id = physicians.id)))
LEFT JOIN patients ON ((patients.id = appointments.patient_id)));
SQL
# test
RSpec.describe ExtendedPhysician, type: :model do
describe 'patiens relation' do
it 'retunrs correct patiens' do
patient1 = Patient.create!
patient2 = Patient.create!
Physician.create!(patients: [patient1, patient2])
expect(ExtendedPhysician.count).to eq(2)
# this not pass because relations ids are same like [patien1.id, patient1.id]
expect(ExtendedPhysician.all.map(&:patient).pluck(:id)).to eq([patient1.id, patient2.id])
end
end
end
And as you can see this test will not pass.
I would like to achieve exactly that this test passes it does not have to be a has_one relation it may be some method in the model but I do not know how to do it right

Related

Creating Custom Friendship Associations Based Around an "Event" Model

I've been researching friendship models using roles, custom associations, etc. But I haven't been able to connect my project to the concepts in a clear way.
I want a "User" to be able to create an event I'm calling a "Gather". A User can also attend a Gather created by other Users. By attending a Gather, the "User" can also be a "Gatherer".
The list of Gatherers will technically be considered friends of the "creator". This is how far I've gotten:
Models:
User
Gather
Gatherer (?)
User
class User < ApplicationRecord
has_many :gathers_as_creator,
foreign_key: :creator_id,
class_name: :Gather
has_many :gathers_as_gatherer,
foreign_key: :gatherer_id,
class_name: :Gather
end
Gather
class Gather < ApplicationRecord
belongs_to :creator, class_name: :User
belongs_to :gatherer, class_name: :User
end
My question is, do I need to a join table, such as Gatherer, to allow multiple attendees and then later pull a friend list for the user/creator ?
Gatherer
belongs_to :gather_attendee, class_name: "User"
belongs_to :attended_gather, class_name: "Gather"
Here's what I think that schema would look like:
create_table "gatherers", force: :cascade do |t|
t.bigint "attended_gather_id"
t.bigint "gather_attendee_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["attended_gather_id"], name: "index_gatherers_on_attended_gather_id"
t.index ["gather_attendee_id"], name: "index_gatherers_on_gather_attendee_id"
end
Help, my head is spinning trying to understand the connections and how to proceed.
Previous planning:
Schema:
create_table "activities", force: :cascade do |t|
t.string "a_type"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "gatherers", force: :cascade do |t|
t.bigint "attended_gather_id"
t.bigint "gather_attendee_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["attended_gather_id"], name: "index_gatherers_on_attended_gather_id"
t.index ["gather_attendee_id"], name: "index_gatherers_on_gather_attendee_id"
end
create_table "gathers", force: :cascade do |t|
t.integer "creator_id"
t.integer "activity_id"
t.text "gather_point"
t.boolean "active"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "interest_gathers", force: :cascade do |t|
t.string "gather_id"
t.string "interest_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "interests", force: :cascade do |t|
t.string "i_type"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "users", force: :cascade do |t|
t.string "username"
t.string "img"
t.string "first_name"
t.string "last_name"
t.string "state"
t.string "city"
t.string "bio"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
add_foreign_key "gatherers", "gathers", column: "attended_gather_id"
add_foreign_key "gatherers", "users", column: "gather_attendee_id"
end
class User < ActiveRecord::Base
has_many :gatherers, foreign_key: gather_attendee_id
has_many :attended_gathers, through: :gatherers
has_many :created_gathers, foreign_key: :creator_id, class_name: "Gather"
end
class Gather < ActiveRecord::Base
has_many :gatherers, foreign_key: :attended_gather_id
has_many :attendees, through: :gatherers, source: :gather_attendee
belongs_to :creator, class_name: "User"
end
class Gatherer < ActiveRecord::Base
belongs_to :gather_attendee, class_name: "User"
belongs_to :attended_gather, class_name: "Gather"
end
The naming here is not great. When naming your models choose nouns as models represent the actual things in your buisness logic - choosing verbs/adverbs makes the names of your assocations very confusing.
class User < ApplicationRecord
has_many :gatherings_as_creator,
class_name: 'Gathering',
foreign_key: :creator_id
has_many :attendences
has_many :gatherings,
through: :attendences
end
# think of this kind of like a ticket to an event
# rails g model Attendence user:references gathering:references
class Attendence < ApplicationRecord
belongs_to :user
belongs_to :gathering
end
# this is the proper noun form of gather
class Gathering < ApplicationRecord
belongs_to :creator,
class_name: 'User'
has_many :attendences
has_many :attendees,
though: :attendences,
class_name: 'User'
end
My question is, do I need to a join table, such as Gatherer, to allow multiple attendees and then later pull a friend list for the user/creator ?
Yes. You always need a join table to create many to many assocations. Gatherer is a pretty confusing name for it though as that's a person who gathers things.
If you want to get users attending Gatherings created by a given user you can do it through:
User.joins(attendences: :groups)
.where(groups: { creator_id: user.id })
You're on the right track.
If I understand what you're looking for correctly, you want a Gather to have many Users and a User to have many Gathers (for the attending piece). So you need a join table like this (this is similar to your gatherers table, but is in a more conventional Rails style):
create_join_table :gathers, :users do |t|
t.index [:gather_id, :user_id]
t.index [:user_id, :gather_id]
end
And then you'd want your User model to be like this:
class User < ApplicationRecord
has_many :gathers_as_creator, foreign_key: :creator_id, class_name: "Gather"
has_and_belongs_to_many :gathers
end
class Gather < ApplicationRecord
belongs_to :creator, class_name: "User"
has_and_belongs_to_many :users
end
(You can change the name of that :users association if you really want, by specifying extra options -- I just like to keep to the Rails defaults as much as I can.)
That should be the bulk of what you need. If you want to pull all the friends of a creator for a specific gather, you would just do gather.users. If you want to pull all of the friends of a creator across all their gathers, that will be:
creator = User.find(1)
friends = User.joins(:gathers).where(gathers: { creator: creator }).all

Error seeding database in Rails error on attribute I'm not using

I am working on the flight-booker project in The Odin Project. I am trying to seed a database with 60 days worth of flights to and from 10 cities. When I seed the database, I get this error:
Created database 'flight_booker_development' Created database
'flight_booker_test'
#Airport:0x00007f003f0c6f20 rails aborted! ActiveModel::MissingAttributeError: can't write unknown attribute
flight_id
raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
^^^^^ /home/stuart/repos/flight-booker/db/seeds.rb:23:in `block (4 levels) in <main>'
/home/stuart/repos/flight-booker/db/seeds.rb:22:in times' /home/stuart/repos/flight-booker/db/seeds.rb:22:in block (3 levels)
in ' /home/stuart/repos/flight-booker/db/seeds.rb:21:in times' /home/stuart/repos/flight-booker/db/seeds.rb:21:in block (2 levels)
in ' /home/stuart/repos/flight-booker/db/seeds.rb:15:in block in <main>' /home/stuart/repos/flight-booker/db/seeds.rb:14:in '
Tasks: TOP => db:reset => db:setup => db:seed (See full trace by
running task with --trace)
The problem seems to be that it is trying to write an attribute that I am not using. Here is my seeds.rb file:
require "faker"
airports = %w[LAX DFW NYC DEN BOS MIA HOU PIT POR MIN]
airports.each { |city_code| Airport.create!(city_code: city_code) }
Airport.all.each do |departure|
Airport.all.each do |arrival|
next if departure == arrival
puts departure
duration = rand(100..300)
flight_number = rand(1000..1999)
frequency = rand(3..5)
60.times do
frequency.times do
Flight.create!(
origin_id: departure,
destination_id: arrival,
duration: duration,
flight_number: flight_number,
departure_time: Faker::Time.forward(days: 60, period: :all)
)
end
end
end
end
I was trying to use flight_id to hold the flight number, but have changed that because I realized I had a flight_id column in my bookings table. I am not doing anything with the bookings table at this time though. I was getting this same error before and then I did a migration to remove flight_id and add_flight number to the flights table. Here is the current schema.rb
ActiveRecord::Schema[7.0].define(version: 2022_12_09_210422) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "airports", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "city_code"
end
create_table "bookings", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "user_id", null: false
t.bigint "flight_id", null: false
t.index ["flight_id"], name: "index_bookings_on_flight_id"
t.index ["user_id"], name: "index_bookings_on_user_id"
end
create_table "flights", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "destination"
t.string "origin"
t.datetime "departure_time"
t.integer "duration"
t.bigint "destination_id", null: false
t.bigint "origin_id", null: false
t.integer "flight_number"
t.index ["destination_id"], name: "index_flights_on_destination_id"
t.index ["origin_id"], name: "index_flights_on_origin_id"
end
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
add_foreign_key "bookings", "flights"
add_foreign_key "bookings", "users"
add_foreign_key "flights", "airports", column: "destination_id"
add_foreign_key "flights", "airports", column: "origin_id"
end
Here the models/flight.rb file:
class Flight < ApplicationRecord
belongs_to :booking, foreign_key: "flight_id"
has_one :origin_id, class_name: "Airport"
has_one :destination_id, class_name: "Airport"
validates :departure_time, presence: true
validates :duration, presence: true
validates :flight_number, presence: true
end
That is the only place that flight_id appears anywhere, but I still get the same error if I remove that line of code.
In the error it is referencing the seeds file line 23, which is the start of the Flight.create action. It did previously try to create flight_id, but that has been changed to flight_number, and I have saved the file, and restarted the computer to be thorough.
Here is the flights_controller.rb file:
class FlightsController < ApplicationController
before_action :set_flight
def index
#flight = Flight.all
end
def new
#flight = Flight.new
end
def create
#flight = Flight.new(flight_params)
end
private
def set_flight
#flight = Flight.find(params[:id])
end
def flight_params
params.require(:flight).permit(
:airport,
:flight_number,
:origin,
:origin_id,
:destination_id,
:destination,
:duration,
:departure_time,
:arrival_time
)
end
end
I previously had flight_id as a permitted param, but that has been changed to flight_number.
So, I'm at a bit of a loss as to what to try next. Any help would be greatly appreciated. I would be happy to provide any additional information you might think is relevant. Thank you.
Edit to add that I tried creating a table entry from the rails console and got the same error in irb.
irb(main):001:0> Flight.create!(departure_time: "2022-12-25 11:11:11
-0700", duration: 200, flight_number: 1599, origin_id: Airport.first, destination_id: Airport.last) Airport Load (0.2ms) SELECT
"airports".* FROM "airports" ORDER BY "airports"."id" ASC LIMIT $1
[["LIMIT", 1]] Airport Load (0.1ms) SELECT "airports".* FROM
"airports" ORDER BY "airports"."id" DESC LIMIT $1 [["LIMIT", 1]]
/home/stuart/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activemodel-7.0.4/lib/active_model/attribute.rb:211:in
with_value_from_database': can't write unknown attribute flight_id`
(ActiveModel::MissingAttributeError)
raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
^^^^^ irb(main):002:0>
The problem is that you have defined your assocation with foreign_key: "flight_id"
class Flight < ApplicationRecord
belongs_to :booking, foreign_key: "flight_id"
end
This option is documented as:
Specify the column used to store the associated object's type
Needless to say having a flights.flight_id column that references bookings is just wrong.
Lets scrub this broken attempt and try again. Stash you work in GIT and roll back and lets setup those models correctly.
To setup the assocation just between flights, airports and airlines you should use three tables and associate them like so:
# rails g model airline name
class Airline < ApplicationRecord
has_many :flights
end
# rails g model airport name iata_code
class Airport < ApplicationRecord
has_many :flights_as_origin,
class_name: 'Flight',
foreign_key: :origin_id
has_many :flights_as_destination,
class_name: 'Flight',
foreign_key: :destination_id
end
# rails g model flight flight_number:string airline:references origin:references destination:references
class Flight < ApplicationRecord
belongs_to :airline
# These should be belongs_to assocations
belongs_to :origin, class_name: 'Airport'
belongs_to :destination, class_name: 'Airport'
end
This is the data thats common to all passengers. You don't alter anything here when a passenger books a ticket. When creating the flights table you need to explicitly tell Rails that destination_id and origin_id should point to the airports table:
class CreateFlights < ActiveRecord::Migration[7.0]
def change
create_table :flights do |t|
# ...
t.references :origin, null: false, foreign_key: { to_table: :airports }
t.references :destination, null: false, foreign_key: { to_table: :airports }
# ...
end
end
end
To model the assocation between passengers, flights and bookings you actually want a many to many assocation with a join table:
# rails g model ticket passenger_name:string flight:references seat:string
class Ticket < ApplicationRecord
belongs_to :flight
end
class Booking < ApplicationRecord
belongs_to :user
has_many :tickets
has_many :flights, through: :tickets
end
class Flight < ApplicationRecord
# ...
has_many :tickets
has_many :bookings, through: :tickets
end
Here you use the tickets table to store each item in an itenary so that you can actually model stuff like multi-leg trips and multiple passengers per booking.

Has_many through query

I have 2 models (Books and Authors) and a third table joining them (has_many through association).
I am trying to implement search in my app and run a query on both tables. My query looks like this and I cannot figure out the problem:
Book.includes(:authors, :author_books).where("books.title LIKE ? OR authors.name = LIKE ?", "%#{book}%", "%#{book}%")
This is the error that I get running it:
PG::UndefinedTable: ERROR: missing FROM-clause entry for table "authors"
SELECT "books".* FROM "books" WHERE (books.title LIKE '%Harry%' OR authors.name = LIKE '%Harry%')
Here is my schema of the three tables:
create_table "author_books", force: :cascade do |t|
t.bigint "author_id"
t.bigint "book_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["author_id"], name: "index_author_books_on_author_id"
t.index ["book_id"], name: "index_author_books_on_book_id"
end
create_table "authors", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "books", force: :cascade do |t|
t.string "title"
t.text "description"
t.string "image"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "rating"
t.string "critics"
t.float "price"
end
author_book.rb
class AuthorBook < ApplicationRecord
validates_presence_of :author, :book
belongs_to :author
belongs_to :book
end
author.rb
class Author < ApplicationRecord
validates :name, uniqueness: true
has_many :author_book
has_many :books, through: :author_book
end
book.rb
class Book < ApplicationRecord
validates :title, uniqueness: true, :case_sensitive => false
has_many :author_book
has_many :authors, through: :author_book
has_many :categories, through: :category_book
def self.search_book(book)
if book
Book.joins(:authors, :author_books).includes(:authors, :author_books).where("books.title LIKE ? OR authors.name = LIKE ?", "%#{book}%", "%#{book}%")
end
end
end
I call this search_book method in my book controller like so:
def search
#books = Book.search_book(params[:book])
end
Some help, please?
Thanks!
From the docs
If you want to add conditions to your included models you’ll have to
explicitly reference them.
That said, you need to add references(:authors) to your query like below to resolve the error
Book.includes(:authors, :author_books).where("books.title LIKE ? OR authors.name = LIKE ?", "%#{book}%", "%#{book}%").references(:authors)
Update:
Can't join 'Book' to association named 'author_books'; perhaps you
misspelled it?
You should replace has_many :author_book with has_many :author_books and through: :author_book with through: :author_books
You forgot to join authors and author_books to your relation. includes loads both :author and :author_books but in separate queries.
Try this:
Book.joins(:authors, :author_books).includes(:authors, :author_books).where("books.title LIKE ? OR authors.name = LIKE ?", "%#{book}%", "%#{book}%")

Rails ActiveRecord Query: Sum total article view count per category

I have articles that are able to be tagged in categories. I am struggling to create a query which will extract the sum of view count (tracked using impressionist gem) of articles within each category.
Schema:
create_table "article_categories", force: :cascade do |t|
t.integer "article_id"
t.integer "category_id"
end
create_table "articles", force: :cascade do |t|
t.string "title"
t.text "description"
t.bigint "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "impressions_count", default: 0
end
create_table "categories", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
Article model
class Article < ApplicationRecord
is_impressionable :counter_cache => true, :unique => false
belongs_to :user
has_many :impressions, as: :impressionable
has_many :article_categories
has_many :categories, through: :article_categories
end
Category model
class Category < ApplicationRecord
has_many :article_categories
has_many :articles, through: :article_categories
end
ArticleCategory model
class ArticleCategory < ActiveRecord::Base
belongs_to :article
belongs_to :category
end
Here is what i have tried so far and the errors when i test in rails console:
cat_group = Article.joins(:article_categories).group_by(&:category_ids)
// this query results in an array of articles within each category
1) cat_group.sum("impressions_count")
//TypeError: no implicit conversion of Array into String
2) cat_group.select("sum(impressions_count) AS total_count")
//ArgumentError: wrong number of arguments (given 1, expected 0)
3) Article.joins(:article_categories).group(:category_id)
//ActiveRecord::StatementInvalid: PG::GroupingError: ERROR: column "articles.id" must appear in the GROUP BY clause or be used in an aggregate function
If I understand you correctly and view count is impressions_count, here is query that you can use:
Article.joins(:categories).group('categories.name').sum(:impressions_count)

Association between two STI/Polymorphic

Currently I have a Group and GroupPeriod that contains the same attributes
create_table "groups", force: :cascade do |t|
t.bigint "company_id"
t.string "name"
t.date "cutoff_date"
t.date "processing_date"
t.integer "working_days"
t.integer "working_hours"
t.integer "status"
t.float "basic_pay"
t.string "type"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["company_id"], name: "index_groups_on_company_id"
end
create_table "group_periods", force: :cascade do |t|
t.bigint "company_id"
t.date "start_date"
t.date "end_date"
t.string "type"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "group_id"
t.index ["company_id"], name: "index_group_periods_on_company_id"
t.index ["group_id"], name: "index_group_periods_on_group_id"
end
The logic is Group has many GroupPeriods. But then I have different groups; Bill and Pay. So I'm creating an STI for both BillGroup and PayGroup:
class Group < ApplicationRecord
has_many :group_periods
end
class BillGroup < Group
#=> has_many :bill_periods??
end
class PayGroup < Group
#=> has_many :pay_periods??
end
The issue I'm having is that each group will have many PayPeriod or BillPeriod. So I created a GroupPeriod to link
class GroupPeriod < ApplicationRecord
belongs_to :group
end
class BillPeriod < GroupPeriod
#=> belongs_to :bill_group??
end
class PayPeriod < GroupPeriod
#=> belongs_to :pay_group??
end
My question is, how can I ensure through inheritance, I can be flexible that
BillGroup has many BillPeriods;
PayGroup has many PayPeriods;
without overlapping (BillGroup will not see PayPeriod and vice versa) with each other? At the same time, is this a bad practice that I should make them into 2 different tables for each BillGroup and PayGroup?
class Group < ApplicationRecord
has_many :group_periods
end
class Period < ApplicationRecord
belongs_to :group
belongs_to :group_periods, polymorphic: true
end
class BillPeriod < GroupPeriod
has_many :periods, as: :group_periods, dependent: :destroy
end
class PayPeriod < GroupPeriod
has_many :periods, as: :group_periods, dependent: :destroy
end
your model looks something like this , rest depends on your associations.

Resources