Convert ActiveRecord habtm query to Arel - ruby-on-rails

I have a pretty common habtm relationship:
Photo has_and_belongs_to_many :tags
Tag has_and_belongs_to_many :photos
In my Photo model I've got a method "with tags" that I use to find a photo that is tagged with a given set of tag_ids. This query needs to match only photos that have all of the given tags, but disregarding the presence or lack of any other tags. Here's my method:
def self.with_terms( array )
select('distinct photos.*').joins(:tags).where('tags.id' => array).group("photos." + self.column_names.join(', photos.')).having("count(*) = #{array.size}")
end
This works as expected.
Now, in order to integrate this better with some other libraries I'm using, I need to re-write this in Arel. (make it an Arel node?, not sure what you normally call this).
I've been experimenting with this, but to be honest I've never tried to use Arel before, so I'm a little lost. I've been experimenting in the console and tried:
t = Photo.arel_table
q = t.join(:tags).on(t[:tags_id].in(array))
Photo.where(q)
But, (1) I don't think q is the right query in the first place, and (2) it creates an Arel::SelectManager, which when passed to a where call raises Cannot visit Arel::SelectManager. So, obviously I'm doing this wrong.
Update: Just to be extra-specific here, I'm looking to return an Arel node, because I'm working with a gem (ransack) that expects you to pass it Arel nodes for search methods. Ransack will chain this Arel node with others in generating complex search queries.
Could an Arel guru show me how do this correctly?

It's hard to find good Arel documentation, but #Philip C has put together some useful slides, referenced in his answer to this question.
The following should be what you're looking for:
photos = Arel::Table.new(:photos)
tags = Arel::Table.new(:tags)
photo_tags = Arel::Table.new(:photo_tags)
q = photos[:id].in(
photos.project(photos[:id])
.join(photo_tags).on(photos[:id].eql(photo_tags[:photo_id]))
.join(tags).on(photo_tags[:tag_id].eql(tags[:id]))
.where(tags[:id].in(array))
.group(photos.columns)
.having(tags[:id].count.eq(array.length))
)
This results in an Arel::Nodes::In instance that you should be able to use directly as in Photo.where(q).
UPDATE:
After looking through the documentation and some of the source for ransack, there doesn't seem to be any natural way to define a custom predicate involving a subquery, which is necessary in your case (because predicates must fit into a where clause). One way to work around this might be to take advantage of the :formatter that your predicate uses as follows:
Ransack.configure do |config|
config.add_predicate 'with_tag_ids',
:arel_predicate => 'in',
:formatter => proc {|tag_ids| tags_subquery(tag_ids) },
:validator => proc {|v| v.present?},
:compounds => true
end
You can define tags_subquery(tag_ids) as a method that generates the arel node as above but replaces array with tag_ids and calls .to_sql on it before returning it (the formatter needs to return a string, not a node).
I haven't tried this, so I'll be thrilled if it works!

Related

activerecord not like query

I could not find an activerecord equivalent of "Not Like". I was able to find a where.not, but that will check if a string does not match a value, as so:
User.where.not(name: 'Gabe')
is the same as:
User.where('name != ?', 'Gabe')
I was looking for a NOT LIKE, where the value is not contained in the string. The equivalent sql query would look like as follows:
SELECT * FROM users WHERE name NOT LIKE '%Gabe%'
In ActiveRecord I can currently get away with the following:
User.where("name NOT LIKE ?", "%Gabe%")
But that leaves a lot to be desired. Any new additions to Rails 4 to facilitate this?
Well, you can do something like:
User.where.not("name LIKE ?", "%Gabe%")
Note: This is only available in Rails 4.
As others have pointed out ActiveRecord does not have a nice syntax for building like statements. I would suggest using Arel as it makes the query less database platform specific (will use ilike for sqlite & like for other platforms).
User.where(User.arel_table[:name].does_not_match('%Gabe%'))
You could also implement this as a scope to contain the implementation to the model:
class User < ActiveRecord::Base
scope :not_matching,
-> (str) { where(arel_table[:name].does_not_match("%#{str}%")) }
end
Unfortunately ActiveRecord does not have a like query builder. I agree that the raw 'NOT LIKE' leaves a lot to be desired; you could make it a scope (scope :not_like, (column, val) -> { ... }), but AR itself does not do this.
Just addition to the answer of "where.not" of active record. "where.not" will exclude null values also. i.e. Query User.where.not(name: 'Gabe') will leave record with name 'Gabe' but it also exclude name column with NULL values. So in this scenario the solution would be
User.where.not(name: 'Gabe')
.or(User.where(name: nil))

Using Rails ActiveRecord to Query Unsaved Objects

Is it possible to query unsaved changes using Rail's ActiveRecord or another similar approach?
An example of a Ruby interactive session is below. What I'd like to see, is the fourth line show a result of '999' instead of '10'. I'm use to using .NET and Entity Framework where something similar to this was possible. Perhaps in Ruby there is a better or different way to do the same thing. I could obviously add up the sum in a loop, but I find the query syntax more elegant. Any help is appreciated.
i = Inventory.where(:product_id => 1)
i.sum(:available) => 10
i.first.available = 999
i.sum(:available) => 10
No, since sum() is actually translated to SQL and run on the db, you must save the record to the db in order for the query to return the result you want.
Alternatively, you can use the Enumerable#sum method in ActiveSupport, which takes a block, like so:
all = Inventory.where(:product_id => 1).to_a
all.first.available = 999
all.sum(&:available)

Remove a 'where' clause from an ActiveRecord::Relation

I have a class method on User, that returns applies a complicated select / join / order / limit to User, and returns the relation. It also applies a where(:admin => true) clause. Is it possible to remove this one particular where statement, if I have that relation object with me?
Something like
User.complex_stuff.without_where(:admin => true)
I know this is an old question, but since rails 4 now you can do this
User.complex_stuff.unscope(where: :admin)
This will remove the where admin part of the query, if you want to unscope the whole where part unconditinoally
User.complex_stuff.unscope(:where)
ps: thanks to #Samuel for pointing out my mistake
I haven't found a way to do this. The best solution is probably to restructure your existing complex_stuff method.
First, create a new method complex_stuff_without_admin that does everything complex_stuff does except for adding the where(:admin => true). Then rewrite the complex_stuff method to call User.complex_stuff_without_admin.where(:admin => true).
Basically, just approach it from the opposite side. Add where needed, rather than taking away where not needed.
This is an old question and this doesn't answer the question per say but rewhere is a thing that exists.
From the documentation:
Allows you to change a previously set where condition for a given attribute, instead of appending to that condition.
So something like:
Person.where(name: "John Smith", status: "live").rewhere(name: "DickieBoy")
Will output:
SELECT `people`.* FROM `people` WHERE `people`.`name` = 'DickieBoy' AND `people`.`status` = 'live';
The key point being that the name column has been overwritten, but the status column has stayed.
You could do something like this (where_values holds each where query; you'd have to tweak the SQL to match the exact output of :admin => true on your system). Keep in mind this will only work if you haven't actually executed the query yet (i.e. you haven't called .all on it, or used its results in a view):
#users = User.complex_stuff
#users.where_values.delete_if { |query| query.to_sql == "\"users\".\"admin\" = 't'" }
However, I'd strongly recommend using Emily's answer of restructuring the complex_stuff method instead.
I needed to do this (Remove a 'where' clause from an ActiveRecord::Relation which was being created by a scope) while joining two scopes, and did it like this: self.scope(from,to).values[:joins].
I wanted to join values from the two scopes that made up the 'joined_scope' without the 'where' clauses, so that I could add altered 'where' clauses separately (altered to use 'OR' instead of 'AND').
For me, this went in the joined scope, like so:
scope :joined_scope, -> (from, to) {
joins(self.first_scope(from,to).values[:joins])
.joins(self.other_scope(from,to).values[:joins])
.where(first_scope(from,to).ast.cores.last.wheres.inject{|ws, w| (ws &&= ws.and(w)) || w}
.or(other_scope(from,to).ast.cores.last.wheres.last))
}
Hope that helps someone

rails/activerecord search eager loaded associations

I have a simple find statement as such:
m = MyModel.find(1, :include => :my_children)
With m.mychildren being an Array; is there anyway to find a particular record from within the array without having to iterate over the entire thing. If I do mychildren.find(1), a new DB query is issues, which doesn't make sense, since they are all loaded already
It looks like there's a little Rails magic going on here. Where Enumerable#find is being overridden by ActiveRecord::Base#find on methods created for associations.
On the upside Enumerable#find is aliased to Enumerable#detect.
Unfortunately Enumerable#find/Enumerable#detect have significantly different syntax from ActiveRecord::Base#find.
So you can't just do mychildren.find(1), instead you've got to do mychildren.detect{|c| c.id == 1} if you want to avoid hitting the database again. You may also want to consider extending Array for a more DRY way of doing this.
class Array
def id_find id
self.detect{|element| element.id == id}
end
end
I'm not quite sure what your asking, but have you tried select:
m.mychildren.select{ |child| child == <<some_statement>> }
This won't hit the database assuming you've used the :include option as you stated in your question.
Alternatively, if you know the number of the child you want, you should be able to just use
m.mychildren[1]

Allow the user to pick a named scope via GET params

In my posts model, I have a named scope:
named_scope :random, :order => "Random()"
I'd like to give users the ability to get posts in a random order by sending a GET request with params[:scope] = 'random'.
Short of eval("Post.#{params[:scope]}"), how can I do this?
I would suggest my very awesome acts_as_filter plugin designed for user-driven filtering of results via named_scopes.
http://github.com/tobyhede/acts_as_filter/tree/master
Eval is fine to use - but make sure you validate against accepted/expected values (I often just plug some values into an array and test accepted_values.include?(parameter))
eval is a pretty bad idea. However, #send is perfect for this - it's inherently safer, and faster than eval (as I understand it).
Product.send(params[:scope])
That should do it :)
I came across it in a search. searchlogic is perfect for this.
I would stay away from eval since you're dealing with data that comes from the user. Maybe just use a simple case statement? This way you'll be able to validate what the data they're giving you.
For the example you give, I'd be explicit, and chain scopes together to build the query you want:
scope = Post
scope = scope.random if params[:scope] == 'random'
#posts = scope.find(:all, ...) # or paginate or whatever you need to do
If params[:scope] isn't 'random', this is the same as calling Post.find(), otherwise it's doing Post.random.find()
From one of the other answers, it looks like find_by_filter would do pretty much the same thing for you.
Using this pattern, you can also combine multiple scopes into the query if you needed to support things that weren't mutually exclusive
e.g.
scope = scope.only_monsters if params[:just_monsters] == 1
scope = scope.limit(params[:limit].to_i) unless params[:limit].to_i.zero?
So GETting /posts?scope=random&just_monsters=1&limit=5 will give you:
Post.random.just_monsters.limit(5).find(:all, ...)

Resources