ActiveRecord scope using class_name - ruby-on-rails

I have a model to keep 2 users "user" and "worker".
create_table "action_logs", :force => true do |t|
t.integer "user_id"
t.integer "worker_id"
t.string "text_log"
end
class ActionLog < ActiveRecord::Base
belongs_to :user
belongs_to :worker, :class_name => 'User'
end
Now, I want to write a scope using "user" and "worker" on this model.
scope :not_inhouse, -> {
includes(:user).where( "users.inhouse = ?", false).
includes(:worker).where( "workers.inhouse = ?", false)
}
But it doesn't work.
ActiveRecord::StatementInvalid:
SQLite3::SQLException: no such column: workers.inhouse:
SELECT COUNT(DISTINCT "action_logs"."id") FROM "action_logs" LEFT OUTER JOIN "users" ON "users"."id" = "action_logs"."user_id" LEFT OUTER JOIN "users" "workers_action_logs" ON "workers_action_logs"."id" = "action_logs"."worker_id" WHERE (users.inhouse = 'f') AND (workers.inhouse = 'f')
It seems ActiveRecord doesn't handle class_name as I expected.
Is there any way to write a scope using class_name?

Thanks for comments. Now I understood 2 points.
Diffrence between includes and joins.
ActiveRecords name joined table name, independently of class_name.
So I can write the scope like this:
scope :not_inhouse, -> {
joins(:user).where( "users.inhouse = ?", false).
joins(:worker).where( "workers_action_logs.inhouse = ?", false)
}
or
scope :not_inhouse, -> {
joins(:user).where( "users.inhouse = ?", false).
joins('INNER JOIN "users" "workers" ON workers.id = action_logs.worker_id').
where( "workers.inhouse = ?", false)
}

Related

Scopes and Queries with Polymorphic Associations

I have found very little about how one can write scopes for polymorphic associations in rails, let alone how to write queries on polymorphic associations.
In the Rails Documentation, I have looked at the Polymorphic Associations section, the Joining Tables section, and the Scopes section. I have also done my fair share of googling.
Take this setup for example:
class Pet < ActiveRecord::Base
belongs_to :animal, polymorphic: true
end
class Dog < ActiveRecord::Base
has_many :pets, as: :animal
end
class Cat < ActiveRecord::Base
has_many :pets, as: :animal
end
class Bird < ActiveRecord::Base
has_many :pets, as: :animal
end
So a Pet can be of animal_type "Dog", "Cat", or "Bird".
To show all the table structures: here is my schema.rb:
create_table "birds", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "cats", force: :cascade do |t|
t.integer "killed_mice"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "dogs", force: :cascade do |t|
t.boolean "sits"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "pets", force: :cascade do |t|
t.string "name"
t.integer "animal_id"
t.string "animal_type"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
I then went ahead and made some records:
Dog.create(sits: false)
Dog.create(sits: true)
Dog.create(sits: true) #Dog record that will not be tied to a pet
Cat.create(killed_mice: 2)
Cat.create(killed_mice: 15)
Cat.create(killed_mice: 15) #Cat record that will not be tied to a pet
Bird.create
And then I went and made some pet records:
Pet.create(name: 'dog1', animal_id: 1, animal_type: "Dog")
Pet.create(name: 'dog2', animal_id: 2, animal_type: "Dog")
Pet.create(name: 'cat1', animal_id: 1, animal_type: "Cat")
Pet.create(name: 'cat2', animal_id: 2, animal_type: "Cat")
Pet.create(name: 'bird1', animal_id: 1, animal_type: "Bird")
And that is the setup! Now the tough part: I want to create some scopes on the Pet model which dig into the polymorphic associations.
Here are some scopes I would like to write:
Give me all the Pets of animal_type == "Dog" that can sit
Give me all the Pets of animal_type == "Cat" that have killed at least 10 mice
Give me all the Pets that are NOT both animal_type "Dog" and cannot sit. (In other words: Give me all the pets: all of them: except for dogs that cannot sit)
So in my Pet model I would want to put my scopes in there:
class Pet < ActiveRecord::Base
belongs_to :animal, polymorphic: true
scope :sitting_dogs, -> {#query goes here}
scope :killer_cats, -> {#query goes here}
scope :remove_dogs_that_cannot_sit, -> {#query goes here} #only removes pet records of dogs that cannot sit. All other pet records are returned
end
I am finding it pretty tough to write these scopes.
Some stuff I found online makes it look like you can only write these scopes with raw SQL. I am wondering if it is possible to use the Hash syntax for these scopes instead.
Any tips/help would be greatly appreciated!
After reviewing previous answers and playing around with it: here is what I got to work.
(Note that Pet.remove_dogs_that_cannot_sit returns an array. This class method is readable but has the drawback of being slow due to N + 1. Any suggestions for fixing that would be greatly appreciated.)
class Dog < ActiveRecord::Base
has_many :pets, as: :animal
scope :sits, -> {where(sits: true)}
end
class Cat < ActiveRecord::Base
has_many :pets, as: :animal
scope :killer, ->{ where("killed_mice >= ?", 10) }
end
class Pet < ActiveRecord::Base
belongs_to :animal, polymorphic: true
scope :by_type, ->(type) {where(animal_type: type)}
scope :by_dogs, -> {by_type("Dog") }
scope :by_cats, -> {by_type("Cat") }
def self.sitting_dogs
all.by_dogs
.joins("INNER JOIN dogs on animal_type = 'Dog' and animal_id = dogs.id")
.merge(Dog.sits)
end
def self.killer_cats
all.by_cats
.joins("INNER JOIN cats on animal_type = 'Cat' and animal_id = cats.id")
.merge(Cat.killer)
end
# returns an Array not Pet::ActiveRecord_Relation
# slow due to N + 1
def self.remove_dogs_that_cannot_sit
all.reject{|pet| pet.animal_type == "Dog" && !pet.animal.sits}
end
end
I'd add these scopes to the relevant individual models eg:
class Dog < ActiveRecord::Base
has_many :pets, as: :animal
scope :sits, ->{ where(sits: true) }
end
class Cat < ActiveRecord::Base
has_many :pets, as: :animal
scope :natural_born_killer, ->{ where("killed_mice >= ?", 10) }
end
if you then need them on the main Pet model, you can just add them as methods eg:
class Pet < ActiveRecord::Base
belongs_to :animal, polymorphic: true
def sitting_dogs
where(:animal => Dog.sits.all)
end
def killer_cats
where(:animal => Cat.natural_born_killer.all)
end
end
etc
Your complicated case is just all pets minus some that are also sitting dogs.
class Pet < ActiveRecord::Base
belongs_to :animal, polymorphic: true
scope :sits, ->{ where(sits: true) }
def sitting_dogs
where(:animal => Dog.sits.all)
end
# There's probably a nicer way than this - but it'll be functional
def remove_dogs_that_cannot_sit
where.not(:id => sitting_dogs.pluck(:id)).all
end
end
I agree of having individual scopes for sitting dogs and killer cats. A scope could be introduced for Pet to filter them by animal_type.
Here's my version:
class Dog < ActiveRecord::Base
has_many :pets, as: :animal
scope :sits, ->{ where(sits: true) }
end
class Cat < ActiveRecord::Base
has_many :pets, as: :animal
scope :killer, ->{ where("killed_mice >= ?", 10) }
end
class Pet < ActiveRecord::Base
belongs_to :animal, polymorphic: true
scope :by_type, -> { |type| where(animal_type: type) }
scope :sitting_dogs, -> { by_type("Dog").sits }
scope :killer_cats, -> { by_type("Cat").killer }
scope :remove_dogs_that_cannot_sit, -> { reject{|pet| pet.animal_type == "Dog" && !pet.animal.sits} }
end
Not the complete answer, but here's a way of executing the remove_dogs_that_cannot_sit query that returns an AR relation and removes the N + 1.
class Pet < ActiveRecord::Base
belongs_to :animal, polymorphic: true
belongs_to :dog, -> { where(pets: { animal_type: 'Dog' }) }, foreign_key: :animal_id
def self.remove_dogs_that_cannot_sit
includes(:dog).where.not("pets.animal_type = 'Dog' AND dogs.sits = false").references(:dogs)
end
def self.old_remove_dogs_that_cannot_sit
all.reject{|pet| pet.animal_type == "Dog" && !pet.animal.sits}
end
end
Using a belongs_to on a polymorphic model is a great way of speeding up certain queries, especially if your polymorphic model is limited to a small number of options. You can clean up some of your scoped method on Pet as well.
def self.sitting_dogs
includes(:dog).merge(Dog.sits).references(:dogs)
end
Faster too.
irb(main):085:0> puts Benchmark.measure { 1000.times { Pet.remove_dogs_that_cannot_sit } }
0.040000 0.000000 0.040000 ( 0.032890)
=> nil
irb(main):087:0> puts Benchmark.measure { 1000.times { Pet.old_remove_dogs_that_cannot_sit } }
1.610000 0.090000 1.700000 ( 1.923665)
=> nil
Here is another way of removing N+1 on remove_dogs_that_cannot_sit
scope :joins_all -> {
joins("left join cats on animal_type = 'Cat' and animal_id = cats.id")
.joins("left join dogs on animal_type = 'Dog' and animal_id = dogs.id")
.joins("left join birds on animal_type = 'Bird' and animal_id = birds.id")
}
Pet.join_all.where.not("animal_type = 'Dog' and sits = 'f'")
What I did is like bellow:
class Dog < ActiveRecord::Base
has_many :pets, as: :animal
scope :sittable, -> {where(sits: true)}
scope :dissittable, -> {where.not(sits: true)}
end
class Cat < ActiveRecord::Base
has_many :pets, as: :animal
scope :amok, ->{ where("killed_mice >= ?", 10) }
end
class Pet < ActiveRecord::Base
belongs_to :animal, polymorphic: true
scope :sitting_dogs, -> do
joins("INNER JOIN dogs on \
pets.animal_id = dogs.id and pets.animal_type = \
'Dog'").merge(Dog.sittable)
end
scope :amok_cats, -> do
joins("INNER JOIN cats on \
pets.animal_id = cats.id and pets.animal_type = \
'Cat'").merge(Cat.amok)
end
scope :can_sit_dogs, -> do
joins("INNER JOIN dogs on \
pets.animal_id = dogs.id and pets.animal_type = \
'Dog'").merge(Dog.dissittable)
end
end
Besides, scope name is more inclined to adjective rather than a noun. So, I use sittable dissitable amok instead of sits killer.
If you are familiar with ransack, you can also it for search based on the issue
Wish helped you.

Convert Complex SQL to Activerecord

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

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')

SQLite3::SQLException: ambiguous column name: id: SELECT DISTINCT id FROM "tickets"

I really don't think the title of this explains well of what I'm trying to do but I'm not even sure how to ask.
So I have ticket has_many tasks and task belongs_to account. I've this as a scope to return the ticket listing where an tickets task belongs to an account:
scope :for_tasks_account, lambda { |account| joins(:tasks => :account ).where("accounts.id = ?", account.id) }
but it's returning multiple of the same ticket because a ticket has multiple tasks that the account belongs to.
How can I get it to only return each ticket once rather for each task in that ticket that an account belongs to?
Thanks!
Update
I'd actually like to combine to scopes to list all that apply to the two lambdas:
scope :for_account, lambda { |account| joins(:group => :accounts ).where("accounts.id = ?", account.id) } || lambda { |account| joins(:tasks => :account ).where("accounts.id = ?", account.id) }
Is this possible? As well as the first issue.
Update 2
I've figured out how to get both of the queries to be combined but I'm still getting multiple of the same ticket in the returned query.
scope :for_group_with_account, lambda { |account| joins(:group => :accounts ).where("accounts.id = ?", account.id) }
scope :for_task_with_account, lambda { |account| joins(:tasks => :account ).where("accounts.id = ?", account.id) }
scope :for_account, lambda { |account| for_group_with_account(account) & for_task_with_account(account).select('DISTINCT id') }
I'm using DISTICNT but I still get
SQLite3::SQLException: ambiguous column name: id: SELECT DISTINCT id FROM "tickets" INNER JOIN "groups" ON "groups"."id" = "tickets"."group_id" INNER JOIN "assignments" ON "groups"."id" = "assignments"."group_id" INNER JOIN "accounts" ON "accounts"."id" = "assignments"."account_id" INNER JOIN "tasks" ON "tasks"."ticket_id" = "tickets"."id" INNER JOIN "accounts" "accounts_tasks" ON "accounts_tasks"."id" = "tasks"."account_id" WHERE ("tickets"."archived" IS NULL) AND (accounts.id = 20) LIMIT 20 OFFSET 0
Thanks again!
I think you should be able to use "distinct" in this scenario.
scope :for_tasks_account, lambda { |account| joins(:tasks => :account ).where("accounts.id = ?", account.id).select('distinct accounts.id') }

Weird relationship behavior on Rails 3 and update_attribute

I'm having a hard time trying to find out why a test is failing:
describe User, "Instance Methods" do
describe "leave_group!" do
it "should set group_id to nil" do
#user = Factory(:user)
#group_2 = Factory(:group, :owner => #user)
#user.leave_group!
#user.reload.group_id.should be_nil # THIS LINE IS FAILING!!!
end
it "should assign another owner to group, if owner was user" do
#user = Factory(:user)
#group = Factory(:group, :owner => #user)
1.upto(4) { #group.add_user Factory(:user) }
#user.leave_group!
#group.reload.owner.should_not eql(#user)
end
end
end
These are the models I'm using:
class User < ActiveRecord::Base
# Associations
has_one :own_group, :class_name => "Group", :foreign_key => "owner_id"
belongs_to :group
def leave_group!
current_group_id, current_group_owner = self.group.id, self.group.owner
self.group_id = nil
save!
Group.find(current_group_id).randomize_owner! if current_group_owner == self
end
end
class Group < ActiveRecord::Base
# Associations
has_many :users
belongs_to :owner, class_name: "User"
def randomize_owner!
current_users = self.users
return false unless current_users.length > 1
begin
new_user = current_users.sort_by { rand }.first
end while self.owner == new_user
self.owner_id = new_user.id
save!
end
end
Am I doing something wrong here? Could I improve it? And more importantly, why is that single test failing?
Here's the log output for runing that single test:
SQL (0.2ms) SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'
AREL (0.3ms) INSERT INTO "users" ("name", "uid", "provider", "email", "image_url", "group_id", "created_at", "updated_at", "timezone", "public_readings", "is_visible_on_leaderboards", "phone_number") VALUES ('John Doe', '314159265', 'facebook', 'john#does.com', NULL, NULL, '2011-07-18 02:02:08.455229', '2011-07-18 02:02:08.455229', NULL, 't', 't', NULL)
Group Load (0.1ms) SELECT "groups".* FROM "groups" WHERE "groups"."key" = 'SNYEMJ' LIMIT 1
AREL (0.1ms) INSERT INTO "groups" ("name", "key", "score", "owner_id", "created_at", "updated_at") VALUES ('John''s Group', 'SNYEMJ', 0, 1, '2011-07-18 02:02:08.522442', '2011-07-18 02:02:08.522442')
AREL (0.0ms) UPDATE "users" SET "group_id" = 1, "updated_at" = '2011-07-18 02:02:08.524342' WHERE "users"."id" = 1
Group Load (0.1ms) SELECT "groups".* FROM "groups" WHERE "groups"."id" = 1 LIMIT 1
User Load (0.2ms) SELECT "users".* FROM "users" WHERE ("users".group_id = 1)
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1
The last 3 lines are all selects, notice rails doesn't even try to remove the group_id from the user. (There are 2 inserts, 1 for the test user and 1 for the test group and 1 update which assigns group_id to the test user).
Try adding a #user.reload call before #user.leave_group in the test.
Even though the user record is updated with it's group in the DB when you create #group_2 from the factory, I suspect the #user object is not. Then you call leave_group! with a #user with a group ID of nil, so the save won't do anything because the object is unchanged. Then in the next line of your test you reload the #user, which now has the group_id from the DB assigned earlier.
try to chance the model class as following:
class User < ActiveRecord::Base
# Associations
has_one :own_group, :class_name => "Group", :foreign_key => "owner_id"
belongs_to :group
def leave_group!
group_randomize_owner
self.group.clear
end
private
def group_randomize_owner
if group.owner == self
group.randomize_owner!
end
end
end

Resources