I tried to ask this question previously and it didn't go well - hopefully I do it better this time.
I have three models
class Flavor < ActiveRecord::Base
has_many :components
has_many :ingredients, through: :components
end
class Ingredient < ActiveRecord::Base
has_many :components
has_many :flavors, through: :components
end
class Component < ActiveRecord::Base
belongs_to :ingredient
belongs_to :flavor
validates :percentage, presence: true
end
Batches are made of flavors, but a flavor can only be made into a batch if it's components add up to 100 percent (hence why I put the percentage validation in there so it was represented).
At first I tried to write this as a scope, but could never get it to work, the model testing i created worked using
def self.batch_eligible
self.find_by_sql("Select flavors.* FROM flavors
INNER JOIN components on flavors.id = components.flavor_id
GROUP BY flavors.id, flavors.name
HAVING SUM(percentage)=100")
end
I did make an attempt at the scope and it failed. Here is the final version of the scope I came up with:
scope :batch_eligible, -> {joins(:components).having('SUM(percentage) = 100').group('flavor.id')}
The resultant object will be used to populate a selection list in a form for batches (flavors can exist before the components are fully worked out).
I figure the limitation here is my understanding of scopes - so how would the scope be built properly to produce the same results as the find_by_sql expression?
All help is appreciated, thanks.
In response to the first comment - I tried a variety of scopes without capturing the errors - the scope above returns this error:
ActiveRecord::StatementInvalid:
PG::UndefinedTable: ERROR: missing FROM-clause entry for table "flavor"
LINE 1: SELECT COUNT(*) AS count_all, flavor.id AS flavor_id FROM "f...
^
: SELECT COUNT(*) AS count_all, flavor.id AS flavor_id FROM "flavors" INNER JOIN "components" ON "components"."flavor_id" = "flavors"."id" GROUP BY flavor.id HAVING SUM(percentage) = 100
changing it to flavors id makes it 'work' but it doesn't return the proper information.
One more piece of code - models testing
require 'rails_helper'
RSpec.describe Flavor, type: :model do
let!(:flavor) {FactoryGirl.create(:flavor)}
let!(:flavor2) {FactoryGirl.create(:flavor)}
let!(:ingredient) {FactoryGirl.create(:ingredient)}
let!(:component) {FactoryGirl.create(:component, flavor: flavor, ingredient: ingredient, percentage: 25)}
let!(:component1) {FactoryGirl.create(:component, flavor: flavor2, ingredient: ingredient, percentage: 100)}
it "should have a default archive as false" do
expect(flavor.archive).to be(false)
end
it "should only have valid flavors for batch creation" do
expect(Flavor.batch_eligible.count).to eq 1
expect(Flavor.batcH_eligible.first).to eq flavor2
end
end
Even with a clean test database - the batch_eligible count is 4 - not one
One more note - the tests DO pass with the find_by_sql function being used - I just think a scope should be possible?
Props to #taryneast for the help - I was pointed in the right direction by this.
After correcting the scope issue with flavors.id - I did run the inspect to see what was happening but I also ran a variety of functions.
puts Flavor.batch_eligible.count or puts Flavor.batch_eligible.size both yield the same thing, for example, the hash {312 => 1} - 312 would be the id of the Factory Created Flavor.
So the problem (once I solved flavors.id) wasn't in the scope - it was in the test. You need to test the LENGTH, Flavor.batch_eligible.length yields the integer 1 that I wanted.
Perhaps everyone else knew that - I didn't.
Thank you Taryn
Related
I get the following error whenever I try to execute find_with_reputation or count_with_reputation methods.
ArgumentError: Evaluations of votes must have scope specified
My model is defined as follows:
class Post < ActiveRecord::Base
has_reputation :votes,
:source => :user,
:scopes => [:up, :down]
The error raises when I try to execute for example:
Post.find_with_reputation(:votes, :up)
or
Post.find_with_reputation(:votes, :up, { order: "likes" } )
Unfortunately, the documentation isn't very clear on how to get around this error. It only states that the method should be executed as follows:
ActiveRecord::Base.find_with_reputation(:reputation_name, :scope, :find_options)
On models without scopes ActiveRecord Reputation System works well with methods such as:
User.find_with_reputation(:karma, :all)
Any help will be most appreciated.
I've found the solution. It seems that ActiveRecord Reputation System joins the reputation and scope names on the rs_reputations table. So, in my case, the reputation names for :votes whose scopes could be either :up or :down are named :votes_up and :votes_down, respectively.
Therefore, find_with_reputation or count_with_reputation methods for scoped models need to be built like this:
Post.find_with_reputation(:votes_up, :all, { conditions: ["votes_up > ?", 0] })
instead of:
Post.find_with_reputation(:votes, :up, { conditions: ["votes_up > ?", 0] })
Note that you'll need to add the conditionsoption to get the desired results, otherwise it will bring all the records of the model instead of those whose votes are positive, for example.
I have a model project and payment
class Project < ActiveRecord::Base
has_many :payments, :dependent => :destroy
end
class Payment < ActiveRecord::Base
belongs_to :project
end
I am trying to find projects which have total sum of its payments higher than is project target amount
#projects=Project.joins(:payments).where('enabled = true and amount < sum(payments.amount)')
this shows me error thay my attempt is too ambiguous
How should I compare field by sum from joined table?
Firstly your error is because of both table have same column name amount you can solve this error using projects.amount but aggregate function i.e sum is not allow in where clause of sql. you can use group by clause with having this way:
#projects=Project.joins(:payments).group("payments.project_id").where('enabled = true').having("sum(payments.amount) > projects.amount")
you can also try this way beacuse PG not support other column to select which are not in group by clause:
#projects=Project.includes(:payments).where("projects.id = payments.project_id and enabled = true").select{ |p| p.amount < p.payements.sum(&:amount) }
ok I've found a solution how to use proposed mysql answer on postgres
Project.select("projects.*").joins(:payments).group("projects.id").where('enabled = true').having("sum(payments.amount) > projects.amount")
found it here
https://github.com/rails/rails/issues/1515
I would like to convert this query using symbols and table_name cause my DB isn't in rails standard.
SELECT v.taux, sum(v.basevat) as tax, sum(v.valuevat) as amountvat, count(t.id)
FROM vatsomething as v
INNER JOIN somethingreceipt as t
ON v.uid_receipt = t.uid
WHERE t.cancelled = 0
GROUP BY v.taux
ORDER BY v.taux ASC;
class Vat < ActiveRecord::Base
self.table_name = "vatsomething"
alias_attribute :base, :basevat
alias_attribute :value, :valuevat
alias_attribute :rate, :taux
belongs_to :receipt, foreign_key: 'uid_receipt', primary_key: 'uid'
end
class Receipt < ActiveRecord::Base
self.table_name = "somethingreceipt"
has_many :item, foreign_key: 'uid_receipt', primary_key: 'uid'
end
I don't have the choice, if I divide this query to 4 queries, it's too slow.
I tried some queries :
Vat.joints(:receipt).
select(:rate, sum(:base), sum(:value), count(Receipt.table_name.:id) ).
where(Receipt.table_name => hash_of_conds.rejectblank)
I've tried quickly with pluck but I don't know if I can use symbols.
I understand that my query is really hard and the non standard db doesn't help.
Maybe, I'll have to use const_get...
Can you help me ?
Thank you.
Unfortunately you cannot use the more railsy methods very easily with legacy databases in rails. Your queries will have to end up being more SQL for this to function correctly. (I have run into this a lot with people that want to keep their DB but run it on rails)
Have you tried this
Vat.joins(:receipt).where("somethingreceipt.cancelled = 0").
group("vatsomething.taux").
select("vatsomething.taux as rate,
sum(vatsomething.basevat) as tax,
sum(vatsomething.valuevat) as amountvat,
count(somethingreceipt.id) as receipt_count").
order("vatsomething.taux")
This should return your desired result.
You have to remember that aliasing methods does not alter the names in the table when running queries you will still have to use the legacy table names and column names.
You can then access the attributes of each object in the ActiveRecord::Relation though their as names e.g. (#rate,#tax,#amountvat,#receipt_count)
Scoping option great for legacy DB's to keep your specific queries consolidated inside the model making it easier to make changes without having to find these queries all over the application.
class Vat
scope :receipt_summary, ->(conditions){
joins(:receipt).where(conditions).
group("vatsomething.taux").
select("vatsomething.taux as rate,
sum(vatsomething.basevat) as tax,
sum(vatsomething.valuevat) as amountvat,
count(somethingreceipt.id) as receipt_count").
order("vatsomething.taux")
}
end
Then you can call Vat.receipt_summary("somethingreceipt.cancelled = 0") or even should be able to call Vat.receipt_summary({receipt:{cancelled:0}})
I have 2 models:
DeliverySlot has_many :orders
Order belongs_to :delivery_slot
Delivery Slots have a limit of how many orders they can hold. I want to create a scope to give all the available delivery slots. An available delivery slot is one that hasn't reached it's limit of associated orders.
My attempt looks like:
scope :available, where("limit > ?", order.count).joins(:orders)
order.count is pseudocode above.
To do this like you have setup you would need to use orders.count instead of order.count because you're referring to the association. This would prompt ActiveRecord to assemble a query that looks something like SELECT COUNT(*) FROM orders WHERE delivery_slot_id = 1.
Rails is actually smart enough to then use that as a subquery in your where condition when you pass it appropriately, a la where('limit > ', orders.count). But as you might see, this won't work if it's precompiled because the query uses an explicit ID in the condition.
What you need instead is to count orders with an ambiguous condition, then use it as a subquery: where('limit > ?', Order.where(delivery_slot_id: 'delivery_slots.id').count). If you tried to run the query for the order count on its own it would fail on delivery_slots, but because it's in the subquery here you should be smooth sailing.
I'd like to propose another way of doing this altogether though, using a counter cache:
class AddCounterCacheToDeliverySlots < ActiveRecord::Migration
class DeliverySlot < ActiveRecord::Base; end
def change
add_column :delivery_slots, :orders_count, :integer, default: 0
add_index :delivery_slots, [:orders_count, :limit]
DeliverySlot.reset_column_information
DeliverySlot.find_each do |delivery_slot|
DeliverySlot.update_counters delivery_slot.id, orders_count: delivery_slot.orders.count
end
end
end
class Order < ActiveRecord::Base
belongs_to :delivery_slot, counter_cache: true
end
class DeliverySlot < ActiveRecord::Base
has_many orders
scope :available, where('orders_count < limit')
end
Rails will automatically increment and decrement the orders_count column for each DeliverySlot, and because it's indexed, it's ridiculously fast to query.
scope :available, lambda {
|delivery_slot| joins(:orders).
where("limit > ?", order.count)
}
try this
So I found a way to do it in SQL. If anyone knows of a more ruby way without creating loads of database queries please jump in.
scope :available, joins('LEFT JOIN orders
ON orders.delivery_slot_id = delivery_slots.id')
.where("delivery_slots.limit > (
SELECT COUNT(*) FROM orders
WHERE orders.delivery_slot_id = delivery_slots.id )
")
I have the following models. Users have UserActions, and one possible UserAction can be a ContactAction (UserAction is a polymorphism). There are other actions like LoginAction etc. So
class User < AR::Base
has_many :contact_requests, :class_name => "ContactAction"
has_many :user_actions
has_many_polymorphs :user_actionables, :from => [:contact_actions, ...], :through => :user_actions
end
class UserAction < AR::Base
belongs_to :user
belongs_to :user_actionable, :polymorphic => true
end
class ContactAction < AR::Base
belongs_to :user
named_scope :pending, ...
named_scope :active, ...
end
The idea is that a ContactAction joins two users (with other consequences within the app) and always has a receiving and a sending end. At the same time, a ContactAction can have different states, e.g. expired, pending, etc.
I can say #user.contact_actions.pending or #user.contact_requests.expired to list all pending / expired requests a user has sent or received. This works fine.
What I would now like is a way to join both types of ContactAction. I.e. #user.contact_actions_or_requests. I tried the following:
class User
def contact_actions_or_requests
self.contact_actions + self.contact_requests
end
# or
has_many :contact_actions_or_requests, :finder_sql => ..., :counter_sql => ...
end
but all of these have the problem that it is not possible to use additional finders or named_scopes on top of the association, e.g. #user.contact_actions_or_requests.find(...) or #user.contact_actions_or_requests.expired.
Basically, I need a way to express a 1:n association which has two different paths. One is User -> ContactAction.user_id, the other is User -> UserAction.user_id -> UserAction.user_actionable_id -> ContactAction.id. And then join the results (ContactActions) in one single list for further processing with named_scopes and/or finders.
Since I need this association in literally dozens of places, it would be a major hassle to write (and maintain!) custom SQL for every case.
I would prefer to solve this in Rails, but I am also open to other suggestions (e.g. a PostgreSQL 8.3 procedure or something simliar). The important thing is that in the end, I can use Rails's convenience functions like with any other association, and more importantly, also nest them.
Any ideas would be very much appreciated.
Thank you!
To provide a sort-of answer to my own question:
I will probably solve this using a database view and add appropriate associations as needed. For the above, I can
use the SQL in finder_sql to create the view,
name it "contact_actions_or_requests",
modify the SELECT clause to add a user_id column,
add a app/models/ContactActionsOrRequests.rb,
and then add "has_many :contact_actions_or_requests" to user.rb.
I don't know how I'll handle updating records yet - this seems not to be possible with a view - but maybe this is a first start.
The method you are looking for is merge. If you have two ActiveRecord::Relations, r1 and r2, you can call r1.merge(r2) to get a new ActiveRecord::Relation object that combines the two.
If this will work for you depends largely on how your scopes are set up and if you can change them to produce a meaningful result. Let's look at a few examples:
Suppose you have a Page model. It has the normal created_at and updated_at attributes, so we could have scopes like:
:updated -> { where('created_at != updated_at') }
:not_updated -> { where('created_at = updated_at') }
If you pull this out of the database you'll get:
r1 = Page.updated # SELECT `pages`.* FROM `pages` WHERE (created_at != updated_at)
r2 = Page.not_updated # SELECT `pages`.* FROM `pages` WHERE (created_at = updated_at)
r1.merge(r2) # SELECT `pages`.* FROM `pages` WHERE (created_at != updated_at) AND (created_at = updated_at)
=> []
So it did combine the two relations, but not in a meaningful way. Another one:
r1 = Page.where( :name => "Test1" ) # SELECT `pages`.* FROM `pages` WHERE `pages`.`name` = 'Test1'
r2 = Page.where( :name => "Test2" ) # SELECT `pages`.* FROM `pages` WHERE `pages`.`name` = 'Test2'
r1.merge(r2) # SELECT `pages`.* FROM `pages` WHERE `pages`.`name` = 'Test2'
So, it might work for you, but maybe not, depending on your situation.
Another, and recommended, way of doing this is to create a new scope on you model:
class ContactAction < AR::Base
belongs_to :user
scope :pending, ...
scope :active, ...
scope :actions_and_requests, pending.active # Combine existing logic
scope :actions_and_requests, -> { ... } # Or, write a new scope with custom logic
end
That combines the different traits you want to collect in one query ...