Retargeting the foreign key in Rails - ruby-on-rails-4

tldr;
In Rails, how do you set the target column of a foreign key to be a column other than its parent’s id?
Efforts so far
This feels like it should be a simple operation, but I’m having little success. I have a parent model, Order, which has many OrderItems, but I want the foreign key of OrderItems to reference a composite of Order’s reference1 and reference2 fields.
I’ve looked at a few paths:
First try
class Order < ActiveRecord::Base
has_many :order_items, foreign_key: :order_reference,
primary_key: :unique_reference
inverse_of: :order
validates :reference1, uniqueness: { scope: :reference2 }
end
class OrderItem < ActiveRecord::Base
belongs_to :order, foreign_key: unique_reference,
primary_key: order_reference
inverse_of: :order_item
end
(Where I created a redundant-feeling unique_reference column, that we populated before creation with reference1+reference2, and gave OrderItem a corresponding order_reference column)
I tried a few variants of the above, but couldn’t persuade the OrderItem to accept unique_reference as the key. It managed to link records, but then when I called OrderItem#order_reference, instead of contents matching the corresponding Order#unique_reference field it would return a stringified version of its parent’s id.
Second try
I removed the unique_reference column from the Order class, replacing it with a method of the same name and a has_many block:
class Order
has_many :order_items, -> (order) { where("order_items.order_reference = :unique_reference", unique_reference: order.unique_reference) }
def unique_reference
"#{reference1}#{reference2}"
end
end
class OrderItem < ActiveRecord::Base
belongs_to :order, ->(item){ where("CONCAT(surfdome_archived_order.source, surfdome_archived_order.order_number) = surfdome_archived_order_items.archived_order_reference") }
end
This time, calling Order#order_items raises a SQL error:
Unknown column 'order_items.order_id' in 'where clause': SELECT order_items.* FROM order_items WHERE order_items.order_id = 1 AND (order_items.order_reference = 'ref1ref2')
Every SQL query I’ve thought to try has the same underlying problem - somewhere, Rails decides we’re still implicitly trying to key by order_id, and I can’t find a way to persuade it otherwise.
Other options
At the moment my options seem to be to use the Composite Primary Keys gem or just ignoring Rails' built-in associations and hacking our own with db queries, but neither seems ideal - and it seems like Rails would surely have an option to switch foreign keys. But if so, where is it?
Thanks,
Sasha

Related

rails_admin get the current entity and display custom enum

I'm trying to customize an enum field with something else than only the name.
For example my entity record from database have columns as: name, postal_code, id etc ..
and I would like to have something like this in the dropdown "#{name} #{postal_code}, #{department}
I'm doing this:
field :city, :enum do
enum do
# Here I would like to get the entity from DB in order to have all
# columns to something similar as =>
entity.collect.each { |c| "#{c.name} (#{c.postal_code}), #
{c.department.name}"}
end
end
but I don't know how to get the active records (entity in my example) of the actual value of City entity.
How can I do this?
note: that department belongs to another model who is associated to City
Considering the OP comments i'm going to asume that the model in question has this association defined like this:
belongs_to :city
And the city field itself something like this
rails_admin do
edit do
field :city
end
end
Because this way rails admin will render a select drop down that will allow you to search the cities without loading them all.
After that on the City model you can define the title method, quoting the docs
By default it tries to call "name" or "title" methods on the record in question. If the object responds to neither, then the label will be constructed from the model's classname appended with its database identifier. You can add label methods (or replace the default [:name, :title]) with:
RailsAdmin.config {|c| c.label_methods << :rails_admin_title }
The title method can then be defined
class City < ApplicationRecord
def rails_admin_title
"#{self.name} (#{self.postal_code}), #{self.department.name}"
end
end
In a related issue, you will probably want to configure how rails admin searches for the city, i'll just link to the docs.
You can do it, but you need to define a name and a (stable)value for each select drop down, a simple hash out to do it like this:
field :city, :enum do
enum do
entity = bindings[:object]
City.all.map do |c|
{c.id => "#{c.name} (#{c.postal_code}), #{c.department.name}"}
end
end
end
Im assuming each city has an id, but you can use any value you want to store on that field on your DB.
So your users would see the nice formatted string, but rails admin would post the form with the city id on the city field.

Active Record callbacks with has_many association

I want to run a before_save or after_add callback every time I add a child object to a parent object (has_many association). In the callback I want to set the end_date property on the parent (cohort) based on the end_date properties of all the children (courses).
class Cohort < ActiveRecord::Base
has_many :courses
before_save :update_end_date
def update_end_date
self.end_date = courses.order(:end_date).last.try(:end_date)
end
end
The problem I'm experiencing is that the courses are not yet persisted to the database in the before_save callback, so courses.order(:end_date) does not return the newly added course(s).
There are several workarounds I could use (e.g. using the Ruby courses.sort_by method or using after_save with update), but my impression is that using the Active Record order method, if possible, would be preferable in terms of efficiency and best practice. Is there a way to do this with Active Record in before_save, or what might be best practice for this? It seems like something that would come up a lot, but I'm having trouble finding solutions that work for me, so I feel like I must be thinking about it wrong. Thanks!
You could do an after save on the courses that may update the cohort if their end date is later than the cohorts end date. and an after destroy on the course, that tells the cohort to update its end date to correspond to the remaining courses.
class Course < ActiveRecord::Base
belongs_to :cohort
after_save :maybe_update_cohort_end_date
after_destroy :update_cohort_end_date
def maybe_update_cohort_end_date
if cohort && self.end_date > cohort.end_date
cohort.end_date = self.end_date
cohort.save
end
end
def update_cohort_end_date
cohort.update_end_date if cohort
end
end
class Cohort < ActiveRecord::Base
has_many :courses
def update_end_date
self.end_date = courses.order(:end_date).last.try(:end_date)
end
end
This way you only make an update if the new or updated course's end date would change the the cohorts end date. But also catches if a course is removed then check what the end date should be

Count on third level associated model

I have the following association:
Reservation
- has_many reservation_occupations
ReservationOccupations
- has_many reservation_occupants
ReservationOccupants
I want to do the following queries:
1 - Get the number of occupants for one reservation
2 - Get the number of occupants for a group of reservations (Reservations.all for example)
Thanks in advance!
1 - Get the number of occupants for one reservation
First, add a has_many :through association from Reservation to ReservationOccupant:
class Reservation < ActiveRecord::Base
has_many :reservation_occupations
has_many :reservation_occupants, through: :reservation_occupations
end
Now you can simply do
reservation = Reservation.first
reservation.reservation_occupants.count
2 - Get the number of occupants for a group of reservations
First, add some more associations:
class ReservationOccupant < ActiveRecord::Base
belongs_to :reservation_occupation
has_one :reservation, through: :reservation_occupation
end
and
class ReservationOccupation < ActiveRecord::Base
belongs_to :reservation
# ...
end
Then, to count the number of occupants for a group of reservations, you can add to your Reservation class the following method:
class Reservation < ActiveRecord::Base
# ...
def self.num_occupants(reservations)
ReservationOccupant
.joins(:reservation_occupation)
.joins(:reservation)
.where("reservations.id": reservations)
.count
end
end
It's worth noting that this num_occupants method works regardless of whether reservations is a collection of reservations or a single reservation. In other words, this method could be used for both of your questions, #1 and #2. However, the first method generates a more efficient SQL query, and is arguably a little clearer, so I'd personally use that when finding the number of occupants for a single reservation.

Group through joined tables Rails 4

I was hoping this would be simple, but in essence I would like to count the number of Users who have certain attributes through a join table in Rails 4.
I have a table called Views which holds three columns:
t.references :viewer
t.references :viewed
t.boolean :sentmessage, default: false
I then have the fields references as:
belongs_to :viewer, :class_name => "User"
belongs_to :viewed, :class_name => "User"
Each user record is then associated with a number of other records like Stats, Questions and a number of others. I'm interested in effectively counting how many viewers of a viewed record are Male or Female (and other search fields) which is data all held in User.stat.gender.name etc.
I'm trying to use a group statement but have no idea how to drill down and count the number of Males etc. I've tried:
#results = View.where(viewed: 63).group("viewer.stat.gender")
But this is so wrong it's frightening.
Any help to do this would be appreciated.
I worked it out finally. For anyone else who is interested:
View.where(viewed_id: 63).joins(viewer: {stat: :gender}).group("name").count
Didn't realise what an INNER JOIN was but some research and some trial and error means I can now show information about the users who have visited.

error on querying nested model attribute

My models:
OrderStatus
belongs_to Order
Order
has_one OrderStatus
belongs_to Logo
Logo
has_many Orders
I would like to perform a query on Logo model attribute named artwork:
OrderStatus.includes({:order => :logo}).where(:order => {:logo => {:artwork => search_artwork}})
but it basically throws an error:
SQLite3::SQLException: no such column: order.logo: SELECT COUNT(DISTINCT "order_statuses"."id") FROM "order_statuses" LEFT OUTER JOIN "orders" ON "orders"."id" = "order_statuses"."order_id" LEFT OUTER JOIN "logos" ON "logos"."id" = "orders"."logo_id" WHERE "order"."logo" = '---
:artwork: xxxxxxx'
I can't see the reason of this error.
EDIT
After extensive searching I realised that where part should be using table names (that is, plurals), so my code should be
#order_statuses = OrderStatus.includes(:order => [:logo]).where(:orders => {:logos => {:artwork => search_artwork}})
but I still see the SQLite3 exception error
SQLite3::SQLException: no such column: orders.logos: SELECT COUNT(DISTINCT "order_statuses"."id") FROM "order_statuses" LEFT OUTER JOIN "orders" ON "orders"."id" = "order_statuses"."order_id" LEFT OUTER JOIN "logos" ON "logos"."id" = "orders"."logo_id" WHERE "orders"."logos" = '---
:artwork: xxxxxxxx
'
So the quick answer to
I still see the SQLite3 exception error ... no such column: orders.logos"
Is that your orders table doesn't have a column named logos, it has a column named logo_id, which was created in your migration you mentioned in your comment.
The reason this error is happening is that Rails doesn't understand (as far as I know) nested values in a .where clause.
Perhaps more importantly, I think what you're (probably) more interested in is:
how do I write logic that will give me all the OrderStatus records related to the Logo with an artwork value equal to search_artwork?
If so, adding a has_one :through relationship to your Logo model will make this really easy!
OrderStatus
belongs_to Order
Order
has_one OrderStatus
belongs_to Logo
Logo
has_many Orders
has_many OrderStatuses, :through => :Order # This is what you're adding!
Then, your query should be as simple as:
Logo.where(artwork: search_artwork).includes(:orderStatus)
EDIT: oops the above is wrong
Logo.where(artwork: search_artwork).order_statuses
For more information on Rails relationships and using has_many :through, see:
http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association