Rails 4 - Complicated query between polymorphic associations - ruby-on-rails

I have a Transaction model, which has a polymorphic association with both Product and Service.
This are the migrations:
create_table :products do |t|
t.string :name
t.timestamps null: false
end
create_table :services do |t|
t.string :name
t.integer :duration
t.timestamps null: false
end
create_table :transactions do |t|
t.integer :amount
t.decimal :price
t.references :sellable, polymorphic: true, index: true
t.references :bill, index: true, foreign_key: true
t.timestamps null: false
end
This are the models
class Transaction < ActiveRecord::Base
belongs_to :bill
belongs_to :sellable, polymorphic: true
def total; price * amount; end
end
class Product < ActiveRecord::Base
has_many :transactions, as: :sellable
end
class Service < ActiveRecord::Base
has_many :transactions, as: :sellable
end
Basically every transaction knows which items was sold, how many units, and how much each unit costed.
How do I get the top 5 object, by units sold?
How do I get the top 5 object, by units money brought-in?

Seems like the use case for custom queries/arel_table
Top 5 object, by units sold?
ANS:
SELECT *, SUM(amount) AS units FROM TRANSACTIONS GROUP BY transactions.sellable_id, transactions.sellable_type ORDER BY units desc LIMIT 5;
Please note that it would work fine in MySQL but might not work in other databases like Postgres. You need to limit the columns in SELECT clause to make it work with them
top 5 object, by units money brought-in
SELECT *, SUM(amount * price) AS money_brought_in FROM TRANSACTIONS
GROUP BY transactions.sellable_id, transactions.sellable_type
ORDER BY money_brought_in desc
LIMIT 5;
You can execute the raw query or using arel whichever you feel comfortable with and can then fetch the respective objects.
Just a rough idea. I hope it helps

Related

Custom Association in ruby on rails

I am finding it hard to understand some of the association defined in the code base.
class Patient < ApplicationRecord
belongs_to :g_district, class_name: "District", primary_key: "id", foreign_key: 'district_id', optional: true
belongs_to :g_perm_district, class_name: "District", primary_key: "id", foreign_key: 'permanent_district_id', optional: true
belongs_to :g_workplc_district, class_name: "District", primary_key: "id", foreign_key: 'workplace_district_id', optional: true
end
class District
belongs_to :province #, optional: true
belongs_to :division, optional: true
has_many :hospitals
has_many :tehsils
has_many :ucs
has_many :mobile_users
has_many :labs
has_many :insecticides
end
I am not clearly getting these kind of associations defined her.(belongs_to :g_district, class_name: "District", primary_key: "id", foreign_key: 'district_id', optional: true).
In my code, there are no models like g_district, g_perm_district, g_workplc_district.
Your code is associating the same model under different names. When in doubt you can always check this by looking at class_name: "District" which is referencing the actual class name.
In your case a Patient can have an association with three different districts (but all of them point to the District model):
3.times { District.create }
patient = Patient.create(
g_district: District.find(1),
g_perm_district: District.find(2),
g_workplc_district: District.find(3)
)
patient.g_district #=> #<District:0x00000001083ce048 id: 1,.. >
patient.g_perm_district #=> #<District:0x00000001083ce048 id: 2,.. >
patient.g_workplc_district #=> #<District:0x00000001083ce048 id: 3,.. >
It might also be worth checking the migrations or schema for Patient.
The schema table might look like this:
create_table "patients", force: :cascade do |t|
t.integer "district_id"
t.integer "permanent_district_id"
t.integer "workplace_district_id"
(...other columns here...)
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
and the migration:
class CreatePatients < ActiveRecord::Migration[7.0]
def change
create_table :patients do |t|
t.integer :district_id
t.integer :permanent_district_id
t.integer :workplace_district_id
(...other columns here...)
t.timestamps
end
end
end
The foreign_key: 'district_id' part (as well as the other two) suggests that these columns must exist and helps ActiveRecord properly associate the same model multiple times
This is simply creating an association with the same table with different names.
Here g_district, g_perm_district, g_workplc_district are associating with District model with the association names: g_dstrict with foreign_key district_id, g_perm_district with foreign_key permanent_district_id and g_workplc_district with foreign_key workplace_district_id
This basically states that Patients can have 3 districts- District, Permanent District, and Workplace District, as all these 3 are types of District, created an association with the same table with a different names.
Refer this blog for more details.

How to set an instance of a model as an attribute of another model in Rails?

I am fairly new to Rails and working on an app that will allow a user to make a List containing their top 5 Items of a certain category. The main issue I'm having is how to keep track of the List order (which should be allowed to change and will be different for each User)?
My Items can belong to many Lists and my Lists should have many Items so, as of now, I am using a has_and_belongs_to_many association for both my Item and List models.
My idea to keep track of the list order right now is to have my #list have 5 attributes: one for each ranking on the list (ie. :first, :second, :third, :fourth, :fifth) and I am attempting to associate the #item instance to the #list attribute (ie. #list.first = #item1, #list.second = #item2 , etc...). Right now I am saving the #list attribute to the #item ID (#list.first = 1), but I would prefer to be able to call the method .first or .second etc and have that point directly at the specific Item instance.
Here is my current schema for lists, items, and the join table list_nominations required for the has_and_belongs_to_many association-which I'm pretty sure I am not utilizing correctly (the :points attribute in items will be a way of keeping track of popularity of an item:
create_table "lists", force: :cascade do |t|
t.integer "user_id"
t.integer "category_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.string "first"
t.string "second"
t.string "third"
t.string "fourth"
t.string "fifth"
end
create_table "items", force: :cascade do |t|
t.string "name"
t.integer "category_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.integer "points", default: 0
end
and here is the code currently in my List and Item models:
class List < ApplicationRecord
belongs_to :user
belongs_to :category
has_and_belongs_to_many :items
end
class Item < ApplicationRecord
belongs_to :category
has_and_belongs_to_many :lists
end
Is there a way to do this or any suggestions on a better way to keep track of the List order without creating multiple instances of the same Item?
I'm afraid your tables don't fit any known approach, you can achieve what you want but this is not a perfect nor a recommended solution, you could specify the primary key on many has_one associations inside lists but in items it's not very possible to have all lists in one association but you can have an instance method which query lists and returns the matched ones
the hacky solution:
class List < ApplicationRecord
belongs_to :user
belongs_to :category
has_one :first_item, primary_key: :first, class_name: "Item"
has_one :second_item, primary_key: :second, class_name: "Item"
has_one :third_item, primary_key: :third, class_name: "Item"
has_one :fourth_item, primary_key: :fourth, class_name: "Item"
has_one :fifth_item, primary_key: :fifth, class_name: "Item"
end
class Item < ApplicationRecord
belongs_to :category
def lists
List.where(
"first = ? OR second = ? OR third = ? OR fourth = ? OR fifth = ?", self.id, self.id, self.id, self.id, self.id
)
end
end
you can read about how to create a many-to-many relationship via has_and_belongs_to_many associations here: https://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association (your tables will need a field to properly point to each other)
What I recommend doing is following a many-to-many through relationship guide (mono-transitive association) :
you will need 1 extra table because you want to track the order(first,second, etc)
DB:
create_table "lists", force: :cascade do |t|
.. all your other fields without first,second, etc..
end
create_table "items", force: :cascade do |t|
.. all your other fields
end
create_table "lists_items", force: :cascade do |t|
t.integer "list_id"
t.integer "item_id"
t.integer "rank" there is where you will store your order (first, second ..) bas as an integer
end
Models:
class ListsItem < ApplicationRecord
belongs_to :list
belongs_to :item
end
class List < ApplicationRecord
belongs_to :user
belongs_to :category
has_many :lists_items, -> { order(:rank) }, limit: 5
has_many :items, through: :lists_items
end
class Item < ApplicationRecord
belongs_to :category
has_many :lists_items
has_many :lists, through: :lists_items
end
you can read more about many-to-many via has_many through here https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
and the difference between the 2 approaches here https://guides.rubyonrails.org/association_basics.html#choosing-between-has-many-through-and-has-and-belongs-to-many

Rails / postgresQL ActiveRecord - HOW and WHY change the primary key?

I have a Rails project with postgresql database.
Let's say I have three models - Student, Teacher and Schedule - that joins a student and teacher
Student Model - Instead of going with the student_id as my primary key, I want to change that to the even_cooler_unique_student_number that a school has for a student.
Teacher Model - typical & traditional.
Schedule Model - I want to associate a Schedule (think - just a math class for now) with one teacher and its students.
How do I do that at the database level and with the AR associations?
What does changing the primary_key do to the database? To my associations through ActiveRecord?
class CreateStudent < ActiveRecord::Migration
def change
create_table :students do |t|
t.integer :unique_cooler_student_id, null: false
t.string :first_name
t.string :last_name
t.timestamps null: false
end
end
end
class CreateTeacher < ActiveRecord::Migration
def change
create_table :teachers do |t|
t.string :first_name
t.string :last_name
t.string :department
t.timestamps null: false
end
end
end
class CreateSchedules < ActiveRecord::Migration
def change
create_table :schedules, id: false, force: true do |t|
t.belongs_to :students, :primary_key => 'unique_cooler_student_id'
t.belongs_to :teachers
t.string :something_else
t.timestamps null: false
end
end
end
class Student
self.primary_key = 'unique_cooler_student_id'
has_many :teachers, through: :classes
end
class Teacher
has_many :students, through: :classes
end
class Schedule
belongs_to :students
belongs_to :teachers
end
Changing the name of primary key usually does very little besides adding a false sense of security - which is only by obscurity.
You can however change the primary key from a auto-incrementing integer to a hash or some other sort of UUID. And there are many valid reasons to do so. This solely changes the method of generating primary keys.
You can even have separate external UUIDs which are used in url params for example. However this does not involve changing the primary key that ActiveRecord uses to join records:
Foo.joins(:bars).find_by(uuid: 'ABCD')
Of course ActiveRecord will let you crack out the tin-foil hat and use whatever primary keys you want - however you will need to specify the primary_key and probably also manually setup the foreign keys in your database to maintain referential integrity. So basically your losing every advantage of convention over configuration for no benefit.
You would have to do it like this:
class CreateSchedules < ActiveRecord::Migration
def change
create_table :schedules, id: false, force: true do |t|
t.references :students, foreign_key: false
t.belongs_to :teachers
t.string :something_else
t.timestamps null: false
end
end
end
class AddStudensIdContraintToSchedules < ActiveRecord::Migration
def change
add_foreign_key :schedules, :students, primary_key: "unique_cooler_student_id"
end
end
class Schedule
has_many :students, primary_key: 'unique_cooler_student_id'
end
This way AR uses WHERE students.unique_cooler_student_id = 2 in the join query.
The only reason you would ever really want to do this this is if you have to use a legacy database and cannot change the database schema.

How to create an ActiveRecord object with a many-to-many relationship

I have created a blank rails app (rails new cheese_shop), with two models and a join table. I am trying to create a cheese shop and specifying which cheeses it contains, at creation time, like this:
cheeses = [
Cheese.create!(name: "Bree"),
Cheese.create!(name: "Kačkavalj"),
]
Shop.create! name: "The Whistling Cheese", cheeses: cheeses
However, I'm getting this error:
SQLite3::ConstraintException: NOT NULL constraint failed: stocks.shop_id: INSERT INTO "stocks" ("cheese_id", "created_at", "updated_at") VALUES (?, ?, ?)
Apparently, the shop ID is not inserted to the stocks table when I create the shop. Is it possible to fix this, without having to do it in two steps (i.e. without first creating the Shop, and then adding the cheeses?)
Here are my models:
class Cheese < ActiveRecord::Base
has_many :shops, through: :stocks
has_many :stocks
end
class Shop < ActiveRecord::Base
has_many :cheeses, through: :stocks
has_many :stocks
end
class Stock < ActiveRecord::Base
belongs_to :shop
belongs_to :cheese
end
My migrations look like this:
class CreateTables < ActiveRecord::Migration
def change
create_table :cheeses do |t|
t.string :name, null: false
t.timestamps null: false
end
create_table :shops do |t|
t.string :name, null: false
t.timestamps null: false
end
create_table :stocks do |t|
t.integer :shop_id, null: false
t.integer :cheese_id, null: false
t.integer :amount
t.float :price
end
end
end
maybe you should try to use nested attributes:
class Shop < ActiveRecord::Base
has_many :cheeses, through: :stocks
has_many :stocks
accepts_nested_attributes_for :stocks
end
and then you will be able to do something like:
cheese = Cheese.create!(name: "Bree")
params = { attrib: { name: "The Whistling Cheese", stocks_attributes: { cheese_id: cheese.id} } }
Shop.create params[:attrib]
here is doc: http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
It turns out Rails creates the associations in two steps, first leaving out the Shop ID, then setting the Shop IDs with an UPDATE, all in one transaction. So The NOT NULL constraints are causing the problem.
Changing this:
t.integer :shop_id, null: false
t.integer :cheese_id, null: false
…to this:
t.integer :shop_id
t.integer :cheese_id, null: false
…solves the problem, although I'm unhappy with this since now I cannot rely on the database to ensure the integrity of my data.

Model associations in RoR

I'm trying to understand how rails works in respect to foreign key and primary keys. Coming from a pure SQL background the Rails method seems very alien to me.
I have the following two migrations:
Groups
class CreateGroups < ActiveRecord::Migration
def self.up
create_table :groups do |t|
t.string :title
t.text :description
t.string :city
t.integer :event_id
t.string :zip
t.string :group_id
t.text :topics
t.timestamps
end
end
def self.down
drop_table :groups
end
end
and Events:
class CreateEvents < ActiveRecord::Migration
def self.up
create_table :events do |t|
t.string :title
t.string :description
t.string :city
t.string :address
t.time :time_t
t.date :date_t
t.string :group_id
t.timestamps
end
end
def self.down
drop_table :events
end
end
A Group can have many events and an event can belong to a single group. I have the following two models:
class Event < ActiveRecord::Base
belongs_to :group, :foreign_key => 'group_id'
end
and
class Group < ActiveRecord::Base
attr_accessible :title, :description, :city, :zip, :group_id, :topics
has_many :events
end
not sure how to specify foreign keys and primary keys to this. For example a group is identified by the :group_id column and using that I need to fetch events that belong to a single group!
how do i do this!
I see you have group_id and event_id as strings in your migration, so I think you might be missing a rails convention. The rails convention is that all tables have a primary key named id of type integer, and any foreign keys reference it by the name of the model, singular, + _id:
table groups:
id: integer
name: string
table events:
id: integer
name: string
group_id: integer
From this convention, all you have to specify in your models is:
class Event < ActiveRecord::Base
belongs_to :group
end
class Group < ActiveRecord::Base
has_many :events
end
At this point, rails knows what to do by convention over configuration: To find an event's group, it knows to look for group_id (singular) to refer to groups.id (plural table name)
event = Event.first #=> returns the first event in database
group = event.group #=> returns the group object
Similarly, it know how to find all the events in a group
group = Group.first #=> returns first group in database
group.events #=> returns an Enumerable of all the events
For more reading, read the rails guide on associations

Resources