I am using cancan to build comprehensive permissions system for my app. I need to be able to "exclude" some specific records even if there is a :read or :manage permissions set on the model e.g.
can :read , User
But I need to exclude specific ids ... something like:
can :read, User except when id in (1,2,3,.... list_of_excluded_ids)
I tried to make this work by building a list_of_excluded_ids like following:
all_ids = User.all
excluded_ids = Excluded.all (This model stores excluded ids)
permitted_id = all_ids - excluded_ids
Then with this list I iterate through setting permission for each id
permitted_ids.each do |permitted_id|
can :read, User, :id => permitted_id
end
This method works when the list of all ids is small. When list is large it carshes mysql with memory errors, as it produces a very long sql query using OR like this:
SELECT `users`.* FROM `users` WHERE ((`users`.`id` = 271) OR ((`users`.`id` = 270) OR ((`users`.`id` = 269) OR ((`users`.`id` = 268) OR ((`users`.`id` = 267) OR ((`users`.`id` = 266) OR ((`users`.`id` = 265) OR ((`users`.`id` = 264) OR ((`users`.`id` = 263) OR .....))))))
The query above is just for example as I cannot post the whole query here. This query becomes so large that it makes mysql crash. Any help is appreciated.
You can pass in a scope to the ability:
can :read, User, User.where(id => permitted_ids) do |u|
permitted_ids.include? u.id
end
Assuming permitted_ids is an array, this will use a SQL "IN" clause to scope your users for index actions. For show actions, CanCan will use the block, passing in the user being accessed. Note the block and the scope do the same thing. This seems silly, but CanCan requires that you provide a block when using a scope that it will use for the show action.
Related
I need an activerecord query to Match ALL items in a params array.
Lets say user has_many roles. and each role has a name.
when i pass ['actor', 'producer', 'singer']. I expect the query to return me the users with all the those three roles or more.
But my method implementation below would return users with having atleast one role name matching to those in the array passed
My current method gives results based on finding any of the tags, not "MATCH ALL"
class User < ActiveRecord::Base
has_many :roles
def self.filter_by_roles(roles)
User.joins(:roles).includes(:roles).where(:roles => {:name => roles})
end
end
I don't want to do any array operations after the query checking if the returned result objects contain all the roles or not. This is because I need the Active Record Relation object to be returned from this.
Thanks in advance.
Try this.
User.joins(:roles).includes(:roles).where(:roles => {:name => roles}).group('usermail').having("COUNT(DISTINCt role_id) = 3")
assuming that field usermail is using to identify users.
You could try this:
def self.filter_by_roles(roles)
scope = User.joins(:roles).includes(:roles)
roles.each do |role|
scope = scope.where(roles: {name: role})
end
scope
end
It's untested, so I'm not sure whether it works.
If you pass role_ids array ([1,2,3]) you can do smth like this:
def self.filter_by_roles(role_ids)
User.select{|user| (role_ids - user.role_ids).empty?}
end
But if you pass roles by title (['actor', 'producer', 'singer']) you need smth like this:
def self.filter_by_roles(roles)
role_ids = Role.find_all_by_title(roles).map(&:id)
User.select{|user| (role_ids - user.role_ids).empty?}
end
I'm trying to add an advanced search option to my app in which the user can search for certain links based on attributes from 3 different models.
My app is set up so that a User has_many :websites, Website has_many :links, and Link has_many :stats
I know how create SQL queries with joins or includes etc in Rails but I'm getting stuck since I only want to retrieve the latest stat for each link and not all of them - and I don't know the most efficient way to do this.
So for example, let's say a user has 2 websites, each with 10 links, and each link has 100 stats, that's 2,022 objects total, but I only want to search through 42 objects (only 1 stat per link).
Once I get only those 42 objects in a database query I can add .where("attribute like ?", user_input) and return the correct links.
Update
I've tried adding the following to my Link model:
has_many :stats, dependent: :destroy
has_many :one_stat, class_name: "Stat", order: "id ASC", limit: 1
But this doesn't seem to work, for example if I do:
#links = Link.includes(:one_stat).all
#links.each do |l|
puts l.one_stat.size
end
Instead of getting 1, 1, 1... I get the number of all the stats: 125, 40, 76....
Can I use the limit option to get the results I want or does it not work that way?
2nd Update
I've updated my code according to Erez's advice, but still not working properly:
has_one :latest_stat, class_name: "Stat", order: "id ASC"
#links = Link.includes(:latest_stat)
#links.each do |l|
puts l.latest_stat.indexed
end
=> true
=> true
=> true
=> false
=> true
=> true
=> true
Link.includes(:latest_stat).where("stats.indexed = ?", false).count
=> 6
Link.includes(:latest_stat).where("stats.indexed = ?", true).count
=> 7
It should return 1 and 6, but it's still checking all the stats rather than the latest only.
Sometimes, you gotta break through the AR abstraction and get your SQL on. Just a tiny bit.
Let's assume you have really simple relationships: Website has_many :links, and Link belongs_to :website and has_many :stats, and Stat belongs_to :link. No denormalization anywhere. Now, you want to build a query that finds, all of their links, and, for each link, the latest stat, but only for stats with some property (or it could be websites with some property or links with some property).
Untested, but something like:
Website
.includes(:links => :stats)
.where("stats.indexed" => true)
.where("stats.id = (select max(stats2.id)
from stats stats2 where stats2.link_id = links.id)")
That last bit subselects stats that are part of each link and finds the max id. It then filters out stats (from the join at the top) that don't match that max id. The query returns websites, which each have some number of links, and each link has just one stat in its stats collection.
Some extra info
I originally wrote this answer in terms of window functions, which turned out to be overkill, but I think I should cover it here anyway, since, well, fun. You'll note that the aggregate function trick we used above only works because we're determining which stat to use based on its ID, which exactly the property we need to filter the stats from the join by. But let's say you wanted only the first stat as ranked by some criteria other than ID, such as as, say, number_of_clicks; that trick won't work anymore because the aggregation loses track of the IDs. That's where window functions come in.
Again, totally untested:
Website
.includes(:links => :stats)
.where("stats.indexed" => true)
.where(
"(stats.id, 1) in (
select id, row_number()
over (partition by stats2.id order by stats2.number_of_clicks DESC)
from stat stats2 where stats2.link_id = links.id
)"
)
That last where subselects stats that match each link and order them by number_of_clicks ascending, then the in part matches it to a stat from the join. Note that window queries aren't portable to other database platforms. You could also use this technique to solve the original problem you posed (just swap stats2.id for stats2.number_of_clicks); it could conceivably perform better, and is advocated by this blog post.
I'd try this:
has_one :latest_stat, class_name: "Stat", order: "id ASC"
#links = Link.includes(:latest_stat)
#links.each do |l|
puts l.latest_stat
end
Note you can't print latest_stat.size since it is the stat object itself and not a relation.
Is this what you're looking for?
#user.websites.map { |site| site.links.map { |link| link.stats.last } }.flatten
For a given user, this will return an array with that contains the last stats for the links on that users website.
I have a simple find in rails 3 that gathers users accounts.
Account.where(:user_id => #user)
The Account model has a 'default' boolean field. As a user adds many accounts I would like the default account to always be first in the loop. Order doesn't seem to work with a boolean field.
Account.where(:user_id => #user, :order => "default DESC")
Is there a way to order the query to handle this or should I just split the queries and find the default account in a separate find?
Try Account.where(:user_id => #user).order("default DESC") - putting :order in your where() clause isn't going to sort the result set.
A cleaner solution might be to add a scope, though.
scope :default_first, order(arel_table[:default].desc)
Then you could just call (assuming your relations are set up properly):
#user.accounts.default_first.all
Quick question about eager loading. I have the following code:
#user =
User.includes(:restaurants).find_by_unique_identifier(params[:unique_identifier])
However, I only want to load restaurants associated with a given user only if the restaurants have a list_id of X (which is one of the columns of the restaurants table).
How would I go about this?
I tried the following, but it still loaded all the restaurant records associated with the user.
#user = User.includes(:restaurants).where(:restaurants => {:list_id => params[:list_id]}).find_by_unique_identifier(params[:unique_identifier])
Assuming you have all the associations requirements setup, then you can use joins:
#user = User.all(:conditions =>["restaurants.list_id = ?", params[:list_id] ], :joins => [:restaurants])
UPDATE:
My code fetches a list of users. However, You can use your filtering methods to narrow down the list or get the individual records.
See the updates at the bottom. I've narrowed this down significantly.
I've also created a barebones app demonstrating this bug: https://github.com/coreyward/bug-demo
And I've also created a bug ticket in the official tracker: https://rails.lighthouseapp.com/projects/8994/tickets/6611-activerecord-query-changing-when-a-dotperiod-is-in-condition-value
If someone can either tell me how to monkey-patch this or explain where this is happening in Rails I'd be very grateful.
I'm getting some bizarre/unexpected behavior. That'd lead me to believe either there is a bug (confirmation that this is a bug would be a perfect answer), or I am missing something that is right under my nose (or that I don't understand).
The Code
class Gallery < ActiveRecord::Base
belongs_to :portfolio
default_scope order(:ordinal)
end
class Portfolio < ActiveRecord::Base
has_many :galleries
end
# later, in a controller action
scope = Portfolio.includes(:galleries) # eager load galleries
if some_condition
#portfolio = scope.find_by_domain('domain.com')
else
#portfolio = scope.find_by_vanity_url('vanity_url')
end
I have Portfolios which can have multiple Galleries each.
The galleries have ordinal, vanity_url, and domain attributes.
The gallery ordinals are set as integers from zero on up. I've confirmed that this works as expected by checking Gallery.where(:portfolio_id => 1).map &:ordinal, which returns [0,1,2,3,4,5,6] as expected.
Both vanity_url and domain are t.string, :null => false columns with unique indexes.
The Problem
If some_condition is true and find_by_domain is run, the galleries returned do not respect the default scope. If find_by_vanity_url is run, the galleries are ordered according to the default scope. I looked at the queries being generated, and they are very different.
The Queries
# find_by_domain SQL: (edited out additional selected columns for brevity)
Portfolio Load (2.5ms) SELECT DISTINCT `portfolios`.id FROM `portfolios` LEFT OUTER JOIN `galleries` ON `galleries`.`portfolio_id` = `portfolios`.`id` WHERE `portfolios`.`domain` = 'lvh.me' LIMIT 1
Portfolio Load (0.4ms) SELECT `portfolios`.`id` AS t0_r0, `portfolios`.`vanity_url` AS t0_r2, `portfolios`.`domain` AS t0_r11, `galleries`.`id` AS t1_r0, `galleries`.`portfolio_id` AS t1_r1, `galleries`.`ordinal` AS t1_r6 FROM `portfolios` LEFT OUTER JOIN `galleries` ON `galleries`.`portfolio_id` = `portfolios`.`id` WHERE `portfolios`.`domain` = 'lvh.me' AND `portfolios`.`id` IN (1)
# find_by_vanity_url SQL:
Portfolio Load (0.4ms) SELECT `portfolios`.* FROM `portfolios` WHERE `portfolios`.`vanity_url` = 'cw' LIMIT 1
Gallery Load (0.3ms) SELECT `galleries`.* FROM `galleries` WHERE (`galleries`.portfolio_id = 1) ORDER BY ordinal
So the query generated by find_by_domain doesn't have an ORDER statement, hence things aren't being ordered as desired. My question is...
Why is this happening? What is prompting Rails 3 to generate different queries to these two columns?
Update
This is really strange. I've considered and ruled out all of the following:
Indexes on the columns
Reserved/special words in Rails
A column name collision between the tables (ie. domain being on both tables)
The field type, both in the DB and Schema
The "allow null" setting
The separate scope
I get the same behavior as find_by_vanity_url with location, phone, and title; I get the same behavior as find_by_domain with email.
Another Update
I've narrowed it down to when the parameter has a period (.) in the name:
find_by_something('localhost') # works fine
find_by_something('name_routed_to_127_0_0_1') # works fine
find_by_something('my_computer.local') # fails
find_by_something('lvh.me') #fails
I'm not familiar enough with the internals to say where the query formed might change based on the value of a WHERE condition.
The difference between the two strategies for eager loading are discussed in the comments here
https://github.com/rails/rails/blob/3-0-stable/activerecord/lib/active_record/association_preload.rb
From the documentation:
# The second strategy is to use multiple database queries, one for each
# level of association. Since Rails 2.1, this is the default strategy. In
# situations where a table join is necessary (e.g. when the +:conditions+
# option references an association's column), it will fallback to the table
# join strategy.
I believe that the dot in "foo.bar" is causing active record to think that you are putting a condition on a table that is outside of the originating model which prompts the second strategy discussed in the documentation.
The two separate queries runs one with the Person model and the second with the Item model.
Person.includes(:items).where(:name => 'fubar')
Person Load (0.2ms) SELECT "people".* FROM "people" WHERE "people"."name" = 'fubar'
Item Load (0.4ms) SELECT "items".* FROM "items" WHERE ("items".person_id = 1) ORDER BY items.ordinal
Because you run the second query against the Item model, it inherits the default scope where you specified order(:ordinal).
The second query, which it attempts eager loading with the full runs off the person model and will not use the default scope of the association.
Person.includes(:items).where(:name => 'foo.bar')
Person Load (0.4ms) SELECT "people"."id" AS t0_r0, "people"."name" AS t0_r1,
"people"."created_at" AS t0_r2, "people"."updated_at" AS t0_r3, "items"."id" AS t1_r0,
"items"."person_id" AS t1_r1, "items"."name" AS t1_r2, "items"."ordinal" AS t1_r3,
"items"."created_at" AS t1_r4, "items"."updated_at" AS t1_r5 FROM "people" LEFT OUTER JOIN
"items" ON "items"."person_id" = "people"."id" WHERE "people"."name" = 'foo.bar'
It is a little buggy to think that, but I can see how it would be with the several different ways you can present a list of options, the way to be sure that you catch all of them would be to scan the completed "WHERE" conditions for a dot and use the second strategy, and they leave it that way because both strategies are functional. I would actually go as far as saying that the aberrant behavior is in the first query, not the second. If you would like the ordering to persist for this query, I recommend one of the following:
1) If you want the association to have an order by when it is called, then you can specify that with the association. Oddly enough, this is in the documentation, but I could not get it to work.
Source: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many
class Person < ActiveRecord::Base
has_many :items, :order => 'items.ordinal'
end
2) Another method would be to just add the order statement to the query in question.
Person.includes(:items).where(:name => 'foo.bar').order('items.ordinal')
3) Along the same lines would be setting up a named scope
class Person < ActiveRecord::Base
has_many :items
named_scope :with_items, includes(:items).order('items.ordinal')
end
And to call that:
Person.with_items.where(:name => 'foo.bar')
This is issue #950 on the Rails GitHub project. It looks like implicit eager loading (which is what causes this bug) has been deprecated in Rails 3.2 and removed in Rails 4.0. Instead, you'll explicitly tell Rails that you need a JOIN for the WHERE clause — e.g.:
Post.includes(:comments).where("comments.title = 'lol'").references(:comments)
If you desperately need this bug fixed in Rails 3.1.*, you can hack ActiveRecord::Relation#tables_in_string to be less aggressive in matching table names. I created a Gist of my (inelegant and slow) solution. This is the diff:
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 30f1824..d7335f3 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
## -528,7 +528,13 ## module ActiveRecord
return [] if string.blank?
# always convert table names to downcase as in Oracle quoted table names are in uppercase
# ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries
- string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_']
+ candidates = string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_']
+ candidates.reject do |t|
+ s = string.partition(t).first
+ s.chop! if s.last =~ /['"]/
+ s.reverse!
+ s =~ /^\s*=/
+ end
end
end
end
It only works for my very specific case (Postgres and an equality condition), but maybe you can alter it to work for you.