I'm working on a Rails project, and trying to create a scope in my model instead of an instance method and preload that scope when needed. But I'm not very experienced with scopes and having trouble getting it to work or may be I'am doing it all wrong. I had an instance method doing the same thing, but noticing some n+1 issues with it. I was inspired by this article How to preload Rails scopes and wanted to try scopes.
(As a side note, I'm using ancestry gem)
I have tried three different ways to create the scope. They all works for Channel.find_by(name: "Travel").depth, but errors out for Channel.includes(:depth) or eager_load.
first try:
has_one :depth, -> { parent ? (parent.depth+1) : 0 }, class_name: "Channel"
2nd try:
has_one :depth, -> (node) {where("id: = ?", node).parent ? (node.parent.depth+1) : 0 }, class_name: "Channel"
3rd try:
has_one :depth, -> {where("id: = channels.id").parent ? (parent.depth+1) : 0 }, class_name: "Channel"
All three works fine in console for:
Channel.find_by(name: "Travel").depth
Channel Load (0.4ms) SELECT "channels".* FROM "channels" WHERE "channels"."name" = $1 LIMIT $2 [["name", "Travel"], ["LIMIT", 1]]
=> 2
..but
Channel.includes(:depth) gives me three different errors for each scope (1st, 2nd, 3rd);
Error for first scope:
NameError (undefined local variable or method `parent' for #<Channel::ActiveRecord_Relation:0x00007fdf867832d8>)
Error for 2nd scope:
ArgumentError (The association scope 'depth' is instance dependent (the scope block takes an argument). Preloading instance dependent scopes is not supported.)
Error for 3rd scope:
Object doesn't support #inspect
What am I doing wrong? Or, what is the best approach? I appreciate your time and help.
I think the .depth method is returning an integer value not associated records. Eager loading is the mechanism for loading the associated records of the objects returned by Model.find using as few queries as possible.
If you want to speed the depth method you need to enable the :cache_depth option. According to ancestry gem documentation:
:cache_depth
Cache the depth of each node in the 'ancestry_depth' column(default: false)
If you turn depth_caching on for an existing model:
- Migrate: add_column [table], :ancestry_depth, :integer, :default => 0
- Build cache: TreeNode.rebuild_depth_cache!
In your model:
class [Model] < ActiveRecord::Base
has_ancestry, cache_depth: true
end
Related
Newbie here!
ruby 2.7.1, gem --version 3.1.4, Rails 6.0.3.4
I'm getting errors on my query
def entry_uniqueness
if Entry.where("lower(from) = ? AND lower(to) = ?",
from.downcase, to.downcase).exists?
errors.add(:base, 'Identical combination already exists. YUP.')
end
end
Error:
PG::SyntaxError: ERROR: syntax error at or near "from" LINE 1: SELECT 1 AS one FROM "entries" WHERE (lower(from) = 'here' A... ^
Full error
app/models/entry.rb:35:in entry_uniqueness' app/controllers/entries_controller.rb:9:in create' Started POST
"/entries" for ::1 at 2021-02-13 16:17:47 +0800 Processing by
EntriesController#create as HTML Parameters:
{"authenticity_token"=>"98sjupNso6NW5xUonE/414I7ZvJQETMPBNWS+jcN+PffHaAJ3K0pdQofVPgnrBfflYn2SDXMlB17Q2G/gzideA==",
"entry"=>{"translation"=>"1", "from"=>"here", "to"=>"there"},
"commit"=>"Post Translation"} [1m[36mEntry Exists? (10.2ms)[0m
[1m[34mSELECT 1 AS one FROM "entries" WHERE (lower(from) = 'here'
AND lower(to) = 'there') LIMIT $1[0m [["LIMIT", 1]] ↳
app/models/entry.rb:35:in `entry_uniqueness' Completed 500 Internal
Server Error in 38ms (ActiveRecord: 10.2ms | Allocations: 2140)
ActiveRecord::StatementInvalid (PG::SyntaxError: ERROR: syntax
error at or near "from" LINE 1: SELECT 1 AS one FROM "entries" WHERE
(lower(from) = 'here' A...
^ ): app/models/entry.rb:35:in entry_uniqueness' app/controllers/entries_controller.rb:9:in create'
What I'm trying to achieve is:
| from | to |
-----------------
| Here | there |
=> validation should prevent the user from adding heRE | THERE but can add THERE | heRE
The columns :from and :to are in the table entries.
Note: I have tried scopes, they work on uniqueness but they fail on case-insensitivity 👇
validates_uniqueness_of :from, scope: :to
Also tried
validates_uniqueness_of :from, :scope => :to, :case_sensitive => false
Also tried #r4cc00n 's implementation but it doesn't work
scope :get_entries_by_from_and_to, ->(from, to) { where(arel_table[:from].lower.eq(from)).where(arel_table[:to].lower.eq(to))}
validates_uniqueness_of :from, if: :entry_uniqueness?
def entry_uniqueness?
Entry.get_entries_by_from_and_to('from','to').nil?
end
from is a reserved name in PostgreSQL. This makes a lot of sense because read queries usually contain this word (think of SELECT value FROM table). Therefore the query that Rails builds from your condition
SELECT 1 AS one FROM "entries" WHERE (lower(from) = 'here' A...
^^^^ ^^^^
confused the database. And btw to is a reserved name too.
One way to avoid running into this issue like this you should consider renaming the column.
Another easy workaround is to just wrap the column name in double-quotes. Just change your condition to this:
Entry.exists?('lower("from") = ? AND lower("to") = ?', from.downcase, to.downcase)
Note that I used double-quotes around the column names and single-quotes around the whole condition.
Besides the column naming issues mentioned by #Spickermann you should consider using a citext (case insentive) type column. It has a number of advantages over your approach:
You don't have to use a verbose query. WHERE foo = ? works regardless of case.
You can declare a multi-column unique index to ensure uniqueness on the database level - which is case insensitive. This prevents potential duplicates due to race conditions.
The drawback is that your application will be less portable if you have to switch to another database like MySQL or Oracle which may not be that much of a real concern.
This of course requires you to be able to enable extensions in your Postgres database and you also need to ensure that you are using the same database in testing, dev and production (which is a good idea anyways).
class EnableCitext < ActiveRecord::Migration
def change
enable_extension("citext")
end
end
class ChangeFoosToCitext < ActiveRecord::Migration
def change
change_column :foos, :bar, :citext
change_column :foos, :baz, :citext
add_index :foos, [:bar, :baz], unique: true
end
end
This will let you use a simple validation with a scope:
validates :bar, uniqueness: { scope: :baz }
You don't need to use the case_sentive: false option which generates an ineffective query.
Here is how I think you can fix your query:
You can define a scope within your Entry model and then use that scope everywhere, something like below:
scope :get_entries_by_from_and_to, ->(from, to) { where(arel_table[:from].lower.eq(from)).where(arel_table[:to].lower.eq(to))}
Then you can use it: Entry.get_entries_by_from_and_to('your_from','your_to') and if that returns nil then there is no records in your DB that matches your condition.
With that said if you want to combine that with what you have and with the validation scope you can do it like below:
def entry_uniqueness?
Entry.get_entries_by_from_and_to('your_from','your_to').nil?
end
validates_uniqueness_of :from, if: :entry_uniqueness?
Be aware that the validates_uniqueness_of is not thread/concurrency safe this means that in a really odd case you can run into some scenarios where you will have not unique data in your DB, to avoid this you should always create a unique index within your DB so the DB will avoid those 'duplicated' scenarios for you.
Hope this helps! 👍
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
The guide does not say what return value would be for association= methods. For example the has_one association=
For the simple case, it returns the assigned object. However this is only when assignment succeeds.
Sometimes association= would persist the change in database immediately, for example a persisted record setting the has_one association.
How does association= react to assignment failure? (Can I tell if it fails?)
Is there a bang! version in which failure raises exception?
How does association= react to assignment failure? (Can I tell if it fails?)
It can't fail. Whatever you assign, it will either work as expected:
Behind the scenes, this means extracting the primary key from this
object and setting the associated object's foreign key to the same
value.
or will save the association as a string representation of passed in object, if the object is "invalid".
Is there a bang! version in which failure raises exception?
Nope, there is not.
The association= should not be able to fail. It is a simple assignment to a attribute on your attribute. There are no validations called by this method and the connection doesn't get persisted in the database until you call save.
The return value of assignments is the value you pass to it.
http://guides.rubyonrails.org/association_basics.html#has-one-association-reference-when-are-objects-saved-questionmark
So another part of the guide does talk about the return behavior for association assignment.
If association assignment fails, it returns false.
There is no bang version of this.
Update
Behaviors around :has_many/has_one through seems to be different.
Demo repository: https://github.com/lulalalalistia/association-assignment-demo
In the demo I seeded some data in first commit, and hard code validation error in second commit. Demo is using rails 4.2
has_many through
class Boss < ActiveRecord::Base
has_many :room_ownerships, as: :owner
has_many :rooms, through: :room_ownerships
end
When I add a room, exception is raised:
irb(main):008:0> b.rooms << Room.first
Boss Load (0.2ms) SELECT "bosses".* FROM "bosses" ORDER BY "bosses"."id" ASC LIMIT 1
Room Load (0.1ms) SELECT "rooms".* FROM "rooms" ORDER BY "rooms"."id" ASC LIMIT 1
(0.1ms) begin transaction
(0.1ms) rollback transaction
ActiveRecord::RecordInvalid: Validation failed: foo
irb(main):014:0> b.rooms
=> #<ActiveRecord::Associations::CollectionProxy []>
has_one through
class Employee < ActiveRecord::Base
has_one :room_ownership, as: :owner
has_one :room, through: :room_ownership
end
When I add a room I don't get exception:
irb(main):021:0> e.room = Room.first
Room Load (0.2ms) SELECT "rooms".* FROM "rooms" ORDER BY "rooms"."id" ASC LIMIT 1
RoomOwnership Load (0.1ms) SELECT "room_ownerships".* FROM "room_ownerships" WHERE "room_ownerships"."owner_id" = ? AND "room_ownerships"."owner_type" = ? LIMIT 1 [["owner_id", 1], ["owner_type", "Employee"]]
(0.1ms) begin transaction
(0.1ms) rollback transaction
=> #<Room id: 1, created_at: "2016-10-03 02:32:33", updated_at: "2016-10-03 02:32:33">
irb(main):022:0> e.room
=> #<Room id: 1, created_at: "2016-10-03 02:32:33", updated_at: "2016-10-03 02:32:33">
This makes it difficult to see whether the assignment succeeds or not.
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 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 ...