Convert Complex SQL to Activerecord - ruby-on-rails

Hi I am trying to work out if its possible to convert this SQL statement to something I can use within a controller on my rails project
SELECT "products".name, properties.display_name,variant_properties.description, variants.price FROM "products"
INNER JOIN "product_properties" ON "product_properties"."product_id" = "products"."id"
INNER JOIN variant_properties on product_properties.property_id = variant_properties.property_id
INNER JOIN "properties" ON "properties"."id" = "product_properties"."property_id" AND properties.id = variant_properties.property_id
INNER JOIN variants on variants.product_id = products.id AND variants.id = variant_properties.variant_id
GROUP BY products.name, variant_properties.description, properties.display_name, variants.price
ORDER BY 1,2,4,3

The simplest way to do it is cheat with the active record join method that accepts raw SQL:
products = Product.select("products.name, properties.display_name, variant_properties.description, variants.price")
.joins("INNER JOIN product_properties ON product_properties.product_id = products.id")
.joins("INNER JOIN variant_properties on product_properties.property_id = variant_properties.property_id")
.joins("INNER JOIN properties ON properties.id = product_properties.property_id AND properties.id = variant_properties.property_id")
.joins("INNER JOIN variants on variants.product_id = products.id AND variants.id = variant_properties.variant_id")
.group("products.name, variant_properties.description, properties.display_name, variants.price")
.order("products.name, properties.display_name, variants.price, variant_properties.description")
With proper associations in your models, you can simplify it to:
products = Product.select("products.name, properties.display_name, variant_properties.description, variants.price")
.joins(:product_properties)
.joins(:variant_properties)
.joins(:properties)
.joins(:variants)
.group("products.name, variant_properties.description, properties.display_name, variants.price")
.order("products.name, properties.display_name, variants.price, variant_properties.description")

Assume you have models:
class Product
has_many :product_properties
has_many :variants
has_many :properties, through: :product_properties
LIST = 'products.name, variant_properties.description, properties.display_name, variants.price'
scope :spec, -> {
joins(:product_properties, :properties, :variants).
merge(Property.strikt).
merge(Variant.strikt).
select(LIST).
group(LIST).
order('1,2,3,4')
}
end
class ProductProperty
belongs_to :product
belongs_to :property
end
class VariantProperty
belongs_to :variant
belongs_to :property
has_many :product_properties, foreign_key: :property_id, primary_key: :property_id
end
class Property
has_many :variant_properties
has_many :product_properties
scope :strikt, -> { joins(:variant_properties).joins(:product_properties) }
end
class Variant
belongs_to :product
has_many :variant_properties
scope :strikt, -> { joins(:product).joins(:variant_properties) }
end
so your SQL you should get as of:
Product.spec.to_sql

Related

In Rails 6, how do I add a condition to a left-outer-joins finder?

I’m using Rails 6.1.4.4. I have this model with a has_many
class MyObject < ApplicationRecord
has_many :products, inverse_of: :application, as: :item
How do I write a scope that does a left outer join and also adds a condition in the LEFT-OUTER-JOIN-ON clause? I have fallen back on raw sql …
scope :awaiting_funding, ->(resubmissions: false) {
joins("LEFT OUTER JOIN products on products.item_id = my_objects.id and products.product_type = 11 and products.item_type = ‘MyObject’”).where('products.id is null')
}
But I would like to convert this to a more Rails-like finder method.
Define a new has_many
class MyObject < ApplicationRecord
has_many :products, inverse_of: :application, as: :item
has_many :my_object_products, -> { where(product_type: 11, item_type: 'MyObject') }, class_name: 'Product'
Now you can define your scope
scope :awaiting_funding, ->(resubmissions: false) {
where.missing(:my_object_products)
}
This will create the query where product_type and item_type are part of the ON in the LEFT OUTER JOIN
PS: use a better name for my_object_products but you get the idea.
Does this work?
scope :awaiting_funding, ->(resubmissions: false) {
left_outer_joins(:products).where(product_type: 11, item_type: 'MyObject', products: { id: nil })
}
I will give you a much generic example of Left Outer Join
Source.
select('a.*', 'count(b.*)').
left_outer_joins(:b).
joins(:c).
where('c.body_parser = ?', true).
group('a.id').
having('count(b.id) = 0').
all
Else, You can also use includes. This will generate a LEFT OUTER JOIN query
MyObject.includes(:products).where(product_type: 11, item_type: 'MyObject', products: { id: nil })

Writing a scope for multiple associations - Rails 4

I am having challenges writing a scope to display:
all cards belonging to events that have payments that belong to a specific user
i am currently able to display, all events that have payments that belong to a specific user using the scope scope :booked_events, -> (user) { joins(payments: :user).where(users: { id: user.id }) } in the event.rb file
some events have a card and some don't
could one kindly advise me how i display all events with a card that
have payments that belong to a specific user
event.rb
has_many :payments
has_one :card
scope :booked_events_with_cards, -> (user) { joins(payments: :user).where(users: { id: user.id }) }
card.rb
belongs_to :event
payment.rb
belongs_to :event
belongs_to :user
user.rb
has_many :payments
i tried the below in the card.rb file but i am unsure
belongs_to :event
has_many :payments, through: :event
scope :cards_belonging_to_booked_events, -> (user) { joins(payments: :event).where(users: { id: user.id }) }
but got the below error:
2.3.0 :012 > cards.cards_belonging_to_booked_events(user)
Card Load (0.6ms) SELECT "cards".* FROM "cards" INNER JOIN "events" ON "events"."id" = "cards"."event_id" INNER JOIN "payments" ON "payments"."event_id" = "events"."id" INNER JOIN "events" "events_payments" ON "events_payments"."id" = "payments"."event_id" WHERE "users"."id" = 4
SQLite3::SQLException: no such column: users.id: SELECT "cards".* FROM "cards" INNER JOIN "events" ON "events"."id" = "cards"."event_id" INNER JOIN "payments" ON "payments"."event_id" = "events"."id" INNER JOIN "events" "events_payments" ON "events_payments"."id" = "payments"."event_id" WHERE "users"."id" = 4
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: users.id: SELECT "cards".* FROM "cards" INNER JOIN "events" ON "events"."id" = "cards"."event_id" INNER JOIN "payments" ON "payments"."event_id" = "events"."id" INNER JOIN "events" "events_payments" ON "events_payments"."id" = "payments"."event_id" WHERE "users"."id" = 4
or, am i to write the scope in event.rb file if i want to display all cards with events that have payments that have been made by a user?
You just need to include card association in joins. It removes events without card associated from the query result:
scope :booked_events_with_cards, -> (user) { joins(:card, payments: :user).where(users: { id: user.id }) }

Rails order by a field in parent belongs_to association

I have three models in my Rails app, User, Number, and Message:
class User < ActiveRecord::Base
has_many :numbers
has_many :messages, through: :numbers
end
class Number < ActiveRecord::Base
belongs_to :user
has_many :messages
end
class Message < ActiveRecord::Base
belongs_to :number
end
Number migration file has:
t.string :digits, index: true # Example: '14051234567' (no + sign)
In my controller:
sort_mode = # asc or desc
#messages = current_user.messages.order(???)
The thing is that I want to sort those messages by their numbers' digits.
How to do that dynamically (depending on sort_mode)?
EDIT:
sort_mode = 'asc'
#messages = current_user.messages.includes(:number)
order = { number: { digits: sort_mode } }
#messages = #messages.order(order)
^ Doesn't work. Second argument must be a direction.
Also, order('number.digits': sort_mode) throws:
SQLite3::SQLException: no such column: messages.number.digits: SELECT "messages".* FROM "messages" INNER JOIN "numbers" ON "messages"."number_id" = "numbers"."id" WHERE "numbers"."user_id" = ? ORDER BY "messages"."number.digits" ASC LIMIT 10 OFFSET 0
You'll need to use includes. Try:
#messages = current_user.messages.includes(:number).order('numbers.digits ASC')

Rails 4 ActsAsTenant complex has_many through doesn't work with setted current_tenant

I have these models in my multi-tenant app:
class Tenant < ActiveRecord::Base
has_many :userroles
has_many :users, through: :userroles
has_many :roles, through: :userroles
has_many :admins, -> { joins(:roles).where("roles.name = 'admin'").uniq }, through: :userroles, class_name: 'User', source: :user
end
class Role < ActiveRecord::Base
has_paper_trail
acts_as_paranoid
has_many :userroles, :dependent => :destroy
has_many :users, :through => :userroles
end
class Userrole < ActiveRecord::Base
acts_as_tenant(:tenant)
has_paper_trail
belongs_to :user
belongs_to :role
end
I use gem ActsAsTenant written by Erwin (source code at github). When current_tenant doesn't set my code work right, but if I set current_tenant, I got errors.
In console I got those errors:
2.1.0 :001 > ActsAsTenant.current_tenant
=> nil
2.1.0 :002 > t = Tenant.first
2.1.0 :004 > t.admins.count
(1.5ms) SELECT DISTINCT COUNT(DISTINCT "users"."id") FROM "users" INNER JOIN "userroles" "userroles_users_join" ON "userroles_users_join"."user_id" = "users"."id" INNER JOIN "roles" ON "roles"."id" = "userroles_users_join"."role_id" AND "roles"."deleted_at" IS NULL INNER JOIN "userroles" ON "users"."id" = "userroles"."user_id" WHERE "users"."deleted_at" IS NULL AND "userroles"."tenant_id" = $1 AND (roles.name = 'admin') [["tenant_id", 1]]
=> 1
2.1.0 :005 > ActsAsTenant.current_tenant = t
2.1.0 :006 > t.admins.count
(2.6ms) SELECT DISTINCT COUNT(DISTINCT "users"."id") FROM "users" INNER JOIN "userroles" "userroles_users_join" ON "userroles_users_join"."user_id" = "users"."id" AND (userroles.tenant_id = 1) INNER JOIN "roles" ON "roles"."id" = "userroles_users_join"."role_id" AND "roles"."deleted_at" IS NULL INNER JOIN "userroles" ON "users"."id" = "userroles"."user_id" WHERE "users"."deleted_at" IS NULL AND "userroles"."tenant_id" = $1 AND (userroles.tenant_id = 1) AND (roles.name = 'admin') [["tenant_id", 1]]
PG::UndefinedTable: ERROR: invalid reference to FROM-clause entry for table "userroles"
LINE 1: ...erroles_users_join"."user_id" = "users"."id" AND (userroles....
^
HINT: Perhaps you meant to reference the table alias "userroles_users_join".
: SELECT DISTINCT COUNT(DISTINCT "users"."id") FROM "users" INNER JOIN "userroles" "userroles_users_join" ON "userroles_users_join"."user_id" = "users"."id" AND (userroles.tenant_id = 1) INNER JOIN "roles" ON "roles"."id" = "userroles_users_join"."role_id" AND "roles"."deleted_at" IS NULL INNER JOIN "userroles" ON "users"."id" = "userroles"."user_id" WHERE "users"."deleted_at" IS NULL AND "userroles"."tenant_id" = $1 AND (userroles.tenant_id = 1) AND (roles.name = 'admin')
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR: invalid reference to FROM-clause entry for table "userroles"
Problem is when I set current_tenant. All works right before setting it. What could be the problem? In gem's code I can't find anything strange.
I change has_many condition:
has_many :admins, -> { for_tenanted_roles.where("roles.name = 'admin'").uniq }, through: :userroles, class_name: 'User', source: :user
And in user.rb
scope :for_tenanted_roles, -> { joins('INNER JOIN "roles" ON "roles"."id" = "userroles"."role_id" AND "roles"."deleted_at" IS NULL') }
It's just handy overrided joins(:roles)

Rails filtering resource one to many through a joint table

I have the following resources:
- restaurant
- category
- item
- check item
Relationship:
class Restaurant < ActiveRecord::Base
has_many :items
has_many :categories
has_many :check_items
class Category < ActiveRecord::Base
belongs_to :restaurant
has_many :items
class Item < ActiveRecord::Base
belongs_to :restaurant
belongs_to :category
class CheckItem < ActiveRecord::Base
belongs_to :item
I need to filter all the check_items of a restaurant where category.uuid = '123123'
so I have my #restaurant.check_items. How do I join these together to basically implement this sql query:
SELECT * from checkitem
INNER JOIN item ON(checkitem.item_id = item.id)
INNER JOIN category ON(category.id = item.category_id)
WHERE category.restaurant_id = 1 AND category.uuid = '123123'
LIMIT 20;
I've tried with scope:
#already have my restaurant resource here with id 1
#restaurant.check_items.by_item_category params[:category_uuid]
And in my models I would have:
class CheckItem < ActiveRecord::Base
...
scope :by_item_category, -> value { joins(:item).by_category value }
class Item < ActiveRecord::Base
...
scope :by_category, -> value { joins(:category).where('%s.uuid = ?' % Category.table_name, value)}
Buut this doesn't seem to work
This appears to be the only way I found this to be working if anyone is interested.
CheckItem.joins(:item => {:category => :restaurant}).where('category.uuid=? and restaurant.id=?', 123123, 1)

Resources