New ez_where plugin.
Posted by ezmobius Mon, 30 Jan 2006 23:31:00 GMT
So me and my friend Fabien Franzen have been busy creating the new ez_where plugin. It's been totally revamped with many additions to the ease of use and the syntax.Big thanks to Fabien for his excellent work extending my original idea and forcing me to rethink this stuf to make it better. Hope someone finds it usefull. As always, this is alpha software so let me knwo of any bugs or any questions you might have.
We have added the ability to do sub queries and nested AND's and OR's. Plus we have replaced find_with_conditions with the shorter ez_find. This method takes a block and can use the :include options to search on multiple models and return them all in one query with your associations included.
These new changes break backwards compatibility, but I think they are worth it. So if you are using the old version of this plugin be ready to refactor a bit ;-). Without further ado, here is the massive README from the distribution.
You can get this version at: opensvn.csie.org/ezra/rails/plugins/dev/ez_where/
Welcome to the new improved ez_where plugin for rails. This plugin is meant
to be used as a nice ruby like syntax for creating the :conditions part of an
ActiveRecord::Base.find. We also add the ActiveRecord::Base.ez_find method.
This method takes a block to simplify single and multi table queries.
articles = Article.ez_find(:all, :include => :author) do |article, author|
article.title =~ "%Foo Title%"
author.any do
name == 'Ezra'
name == 'Fab'
end
end
This will produce :conditions => ["article.title LIKE ? AND
(authors.name = ? OR authors.name = ?)",
"%Foo Title%", "Ezra", "Fab"]
Basically here is the breakdown of how we map ruby operators
to SQL operators:
foo == 'bar' #=> ["foo = ?", 'bar']
foo =~ '%bar' #=> ["foo LIKE ?", '%bar']
foo <=> (1..5) #=> ["foo BETWEEN ? AND ?", 1, 5]
id === [1, 2, 3, 5, 8] #=> ["id IN(?)", [1, 2, 3, 5, 8]]
<, >, >=, <= et all will just work like you expect.
There is also the ability to create the conditions in stages so
you can build up a query:
cond = Caboose::EZ::Condition.new do
foo == 'bar'
baz <=> (1..5)
id === [1, 2, 3, 5, 8]
end
@result = Model.find(:all, :conditions=> cond.to_sql)
#=> ["foo = ? AND baz BETWEEN ? AND ? AND id IN (?)",
"bar", 1, 5, [1, 2, 3, 5, 8]]
You can even do nested sub conditions. condition will use AND
by default in the sub condition:
cond = Caboose::EZ::Condition.new :my_table do
foo == 'bar'
baz <=> (1..5)
id === [1, 2, 3, 5, 8]
condition :my_other_table do
fiz =~ '%faz%'
end
end
@result = Model.find(:all, :conditions=> cond.to_sql)
#=> ["my_table.foo = ? AND my_table.baz BETWEEN ? AND ?
AND my_table.id IN (?) AND (my_other_table.fiz LIKE ?)",
"bar", 1, 5, [1, 2, 3, 5, 8], "%faz%"]
You can also build multiple Condition objects and join
them together for one monster find:
cond_a = Caboose::EZ::Condition.new :my_table do
foo == 'bar'
condition :my_other_table do
id === [1, 3, 8]
foo == 'other bar'
fiz =~ '%faz%'
end
end
#=> ["my_table.foo = ? AND (my_other_table.id IN (?) AND my_other_table.foo = ?
AND my_other_table.fiz LIKE ?)", "bar", [1, 3, 8], "other bar", "%faz%"]
cond_b = Caboose::EZ::Condition.new :my_table do
active == true
archived == false
end
#=> ["my_table.active = ? AND my_table.archived = ?", true, false]
composed_cond = Caboose::EZ::Condition.new
composed_cond << cond_a
composed_cond << cond_b
composed_cond << 'fuzz IS NULL'
@result = Model.find(:all, :conditions => composed_cond.to_sql)
#=> ["(my_table.foo = ? AND (my_other_table.id IN (?) AND my_other_table.foo = ?
AND my_other_table.fiz LIKE ?)) AND (my_table.active = ? AND my_table.archived = ?)
AND fuzz IS NULL", "bar", [1, 3, 8], "other bar", "%faz%", true, false]
You can compose a new condition from different sources:
ar_instance = Author.find(1)
other_cond = Caboose::EZ::Condition.new :my_table do
foo == 'bar'; baz == 'buzz'
end
cond = Caboose::EZ::Condition.new
# another Condition
cond.append other_cond
# an array in AR condition format
cond.append ['baz = ? AND bar IS NOT NULL', 'fuzz'], :or
# a raw SQL string
cond.append 'biz IS NULL'
# an Active Record instance from DB or as Value Object
cond.append ar_instance
#(append is aliased to << because of syntax issues
involving multiple args like :or)
@result = Model.find(:all, :conditions=> cond.to_sql)
#=> ["(my_table.foo = ? AND my_table.baz = ?) OR (baz = ? AND bar IS NOT NULL)
AND biz IS NULL AND authors.id = ?", "bar", "buzz", "fuzz", 1]
OK there is also other options for doing subconditions. OR is
aliased to any and any creates a subcondition that uses OR to
join the sub conditions:
cond = Caboose::EZ::Condition.new :my_table do
foo == 'bar'
any :my_other_table do
baz === ['fizz', 'fuzz']
biz == 'boz'
end
end
@result = Model.find(:all, :conditions=> cond.to_sql)
#=> ["my_table.foo = ? AND (my_other_table.baz IN (?)
OR my_other_table.biz = ?)",
"bar", ["fizz", "fuzz"], "boz"]
OK lets look a bit more at ez_find with a few more complex queries:
# all articles written by Ezra. Here you can use a normal AR object
# in the conditions
# session[:user_id] = 2
ezra = Author.find(session[:user_id])
@articles = Article.ez_find(:all, :include => :author) do |article, author|
author << ezra # use AR instance to add condition; uses PK value if set: author.id = ezra.id
end
#=>["(authors.id = ?)", 2]
# all articles written by Ezra, where he himself responds in comments
@articles = Article.ez_find(:all, :include => [:author, :comments]) do |article, author, comment|
article.author_id == ezra.id
comment.author_id == ezra.id
end
#=>["(articles.author_id = ?) AND (comments.author_id = ?)", 2, 2]
# any articles written by Fab or Ezra
@articles = Article.ez_find(:all, :include => :author) do |article, author|
author.name === ['Fab', 'Ezra']
end
#=>["(authors.name IN (?))", ["Fab", "Ezra"]]
# any articles written by Fab or Ezra, using subcondition
@articles = Article.ez_find(:all, :include => :author) do |article, author|
author.any do
name == 'Ezra'
name == 'Fab'
end
end
#=>["(authors.name = ? OR authors.name = ?)", "Ezra", "Fab"]
# any articles written by or commented on by Fab, using subcondition
@articles = Article.ez_find(:all, :include => [:author, :comments]) do |article, author, comment|
article.sub { author_id == 1 }
comment.outer = :or # set :outer for the comment condition, since it defaults to :and
comment.sub { author_id == 1 }
end
#=>["(articles.author_id = ?) OR (comments.author_id = ?)", 1, 1]
@articles = Article.ez_find(:all, :include => [:author, :comments],
:outer => { :comments => :or },
:inner => { :article => :or}) do |article, author, comment|
article.sub { author_id == 1; author_id == 2 }
comment.sub { author_id == 1 }
end
["(articles.author_id = ? OR articles.author_id = ?) OR (comments.author_id = ?)", 1, 2, 1]
And finally you can use any and all with ez_condition like this:
cond = Article.ez_condition { active == true; archived == false }
cond.all { body =~ '%intro%'; body =~ '%demo%' }
cond.any { title =~ '%article%'; title =~ '%first%' }
#=> ["articles.active = ? AND articles.archived = ?
AND (articles.body LIKE ? AND articles.body LIKE ?)
AND (articles.title LIKE ? OR articles.title LIKE ?)",
true, false, "%intro%", "%demo%", "%article%", "%first%"]
As you can see we can get quite detailed in the queries this can create. Just use your imagination ;-) Also the test cases in the plugin source have many more examples to choose from.
Get it here: ez_where
Searching...





Cool stuff Ezra, this is really way beyond the humble beginnings :-)
Thanks Jens- It has come quite a ways now!
Nice to see you were able to keep this moving after the c.l.r. thread. I'm looking forward to using this on Rails projects in the future. -rcoder (a.k.a lennon)
Wow! This is really lovely. I've always (strangely) been rather fond of SQL, but this looks so much happier in amongst all the other Ruby stuff. Great work!
Big Thanks to rcoder for helping me out in the earlier stages. He gave me the initial inspiration to overide the ruby operators and convert them into sql operators. And the thing has just grown from there.
Thanks rcoder!
Very very cool. However, if you don't mind my two cents: why not find_with_conditions? It's cleaner. After all, we have stuff like options_from_collection_for_select in Rails too. And if you named it find_with_conditions, you could submit a patch and give "the love" (as Rails fanboys say) to everyone.
Rob-
I don't think that this will make it into core. Its a perfect fit for a plugin though. but who knows? Stranger things have happened.
Anyway you are right and i have aliased ez_find so you can use find_with_conditions if you want. They will both work the same way. Recheck out the code to get this change.
I like find_with_conditions but I also wanted something shorter to keep the code lines smaller. you can use either one you want to now.
I would submit it as a patch but its still alpha software. It does have an extensive test suite but i am sure there will be edge cases that will pop up. I was going to just override the normal find method so it could be used normally or with the new block syntax. But other plugins like the Globalize plugin override find already so i decided not to do that.
After this thing gets some wider usage and testing by people I might make a patch to core and see what people think about it. But I know what they will say and I think I agree with them. Things like this belong in a plugin. Plugins are easy to use these days and this on doesn't monkey patch any rails internals. It just builds up the :conditions and sends them along to a normal find. This way it is fast because it isn't really doing any heavy lifting.
Please use this and give feedback so I can improve it. I want it to remain as lightweight as possible. And i am very open to any syntax improvements people offer.
And since there is good test coverage, I am all for patches too ;-)
nice site
Hello Ezra, I wanted to write something like this for a long time. I would like to integrate this in Og (an alternative Ruby ORM system) Please contact me privately if you are interested in this.
ok, and thanks for aliasing it :) Great stuff
this site does not display correctly in firefox... :-(
What doesn't work exactly in firefox? It seems to work fine for me.
Excellent work, Ezra. I had some really ugly query-building code in a drilldown search I built, and this cut the code by 66%. Not to mention a whole lot easier on the eyes. And the site looks great in Firefox. Maybe he doesn't like your design ;)
This is extremely cool. I am wondering if there is any support for case insensitivity? basically i want to do a "LOWER(column) = ?" for certain searches.
very lovely. it'd be great to have some support for building queries in a form, or translating a search @params to these conditions. Really appreciate what you've already done, though.
Hmm, can you give some examples of conditionally adding conditions? (as from a form.) Creating a new condition object for every possible condition, and appending them all together, is a bit ugly.
This is a fantastic plugin. Great work! I'm trying to run a find on a model where I don't know what model it is beforehand. To get the right model, I'm doing an eval: @people = eval(person_type + '.find' + ':all, :conditions => cond') cond is a new conditions object I've created with ez_where. I don't know how to get it to work inside an eval. Any thoughts?
Sean. instead of using a big old eval like that you can use Object.const_get like this:
That is much better then an eval and accomplishes the saem thing but lets you use the cond object in a normal way.
Does that help?
Or even better:
Isn't ruby fun?
That is perfect. I had no idea. Thank you so much!
Hmm, one more question--how do I pass the results of this into a paginator? Or do I not need to and I should make my own paged links?
No, wait, scratch that. I had an error elsewhere. Passing cond.to_sql to the paginator works as I thought it should. My issue now is more related to STI than to the plugin, so I'll take it back to the Rails list. :) Thank you again!
Wow. Ruby (and Rails) never ceases to amaze me. Great work.
Have one remaining issue with ez_where plugin trying to concatenate separate columns to match my aggregation and I guess it boils down to precedence.
my method looks like this...
def list2 fi_first_name = params[:client][:first_name] fi_last_name = params[:client][:last_name] fi_whole_name = params[:client][:wholename] cond = Caboose::EZ::Condition.new do first_name == fi_first_name unless fi_first_name == "" last_name == fi_last_name unless fi_last_name == "" [(first_name||middle_initial||last_name) == fi_whole_name] unless fi_whole_name == "" endand the log shows the problem...that it only searches the 'first_name' column and not the postgresql combination of the 3 columns...
rocessing ClientsController#list2 (for 127.0.0.1 at 2006-03-01 11:57:29) [POST] Parameters: {"commit"=>"Find", "client"=>{"wholename"=>"George Washington", "first_name"=>"", "last_name"=>""}, "action"=>"list2", "controller"=>"clients"} Client Load (0.003730) SELECT * FROM clients WHERE (first_name = 'George Washington') various attempts to use append << ["(first_name||middle_initial||last_name) = ?", fi_wholename] haven't changed the outcomeI do note that I can use the rails method of find with this type of aggregation...
Craig
boy - that came out ugly...I guess I didn't realize that the comments wouldn't respect line breaks/indentations/spacking etc. necessary for code. Sorry
Craig- You can't use the || ops like that on the left hand side of an expression. They get eval'ed by ruby as or statements. Also you have the whole thing wrapped in an array, the rhside and the lhside.
Try this:
cond = Caboose::EZ::Condition.new do first_name == fi_first_name unless fi_first_name == "" last_name == fi_last_name unless fi_last_name == "" end cond<<["(first_name||middle_initial||last_name) = ?", fi_wholename]OK - your way... controller code
This is awesome! I would like to see it integrated into the core, but with a new naming scheme. ez_where is no good, use plain english like "ConditionCollection" or something like that. Actually I typed "class ConditionCollection"... then I went and looked to see if anybody else had already done it. This site is #1 on google for "active-record conditions-object" The :conditions option for Find aught to recognize an object of type conditionscollection, at the most basic, I'd like to be able to do cond = ConditionCollection.new; cond.add(:something => something_obj) MyObjects.find(:all, :conditions => cond)
Jeff- If you look at the docs a bit more you will see that you can already do that.
include Caboose::EZ cond_a = Condition.new :my_table do foo == 'bar' condition :my_other_table do id === [1, 3, 8] foo == 'other bar' fiz =~ '%faz%' end end cond_b = Condition.new :my_table do active == true archived == false end composed_cond = Caboose::EZ::Condition.new composed_cond << cond_a composed_cond << cond_b.to_sql composed_cond << 'fuzz IS NULL' ["(my_table.foo = ? AND (my_other_table.id IN (?) AND my_other_table.foo = ? AND my_other_table.fiz LIKE ?)) AND (my_table.active = ? AND my_table.archived = ?) AND fuzz IS NULL", "bar", [1, 3, 8], "other bar", "%faz%", true, false] MyModel.find(:all, :conditions => composed_cond.to_sql)We are working on a few new additions to this plugin and a new tutorial that covers all the features. Look for it this weekend.When a model has two associations to the same table with different association names, ez_find breaks down because it doesn't use the table alias for the second table. E.g., class Item belongs_to :seller, :class_name=>'User' belongs_to :buyer, :class_name=>'User' end You can't do: Item.ez_find(:all, :include=>[:buyer, :seller]) do |item, buyer, seller| buyer.login =~ 'Ez%' seller.login =~ 'Kevin%' end The ez condition generator creates a where clause like: WHERE ((users.login LIKE 'Ez%') AND (users.login LIKE 'Kevin%')) That second one is actually aliased within the join in Rails 1.1 to "sellers_items". The ez finder should actually generate: WHERE ((users.login LIKE 'Ez%') AND (sellers_items.login LIKE 'Kevin%'))
When a model has two associations to the same table with different association names, ez_find breaks down because it doesn't use the table alias for the second table.
E.g.,
class Item
belongs_to :seller, :class_name=>'User'
belongs_to :buyer, :class_name=>'User'
end
You can't do:
Item.ez_find(:all, :include=>[:buyer, :seller]) do |item, buyer, seller|
buyer.login =~ 'Ez%'
seller.login =~ 'Kevin%'
end
The ez condition generator creates a where clause like:
WHERE ((users.login LIKE 'Ez%') AND (users.login LIKE 'Kevin%'))
That second one is actually aliased within the join in Rails 1.1 to "sellers_items". The ez finder should actually generate:
WHERE ((users.login LIKE 'Ez%') AND (sellers_items.login LIKE 'Kevin%'))
Ezra, I can get the ez_find working for a single table but when I include a second table it seems to breakdown (probably because I am not sure that I have the code correct). This is what I have: @search_result = Voter.ez_find(:all, :include => :residences) do |voter, residences| if params[:voter][:gender] != 'U' if params[:voter][:gender] == 'M' voters.gender == 'M' else voters.gender == 'F' end end voters.lastname =~ params[:voter][:lastname] if params[:voter][:lastname] voters.firstname =~ params[:voter][:firstname] if params[:voter][:firstname] voters.ph_home == params[:voter][:ph_home] if (params[:voter][:ph_home] != '000-000-0000' || params[:voter][:ph_home]) residences do residences.number == params[:residence][:number] if params[:residence][:number] residences.streetname == params[:residence][:streetname] if params[:residence][:streetname] residences.streettype == params[:residence][:streettype] if params[:residence][:streettype] residences.city == params[:residence][:city] if params[:residence][:city] residences.postalcode == params[:residence][:postalcode] if params[:residence][:postalcode] #end end Since I am very new to this I wonder if you could provide some guidance as to syntax or likely pitfalls. Rick
Folks, it looks like rails 1.1 broke ez_find for multiple tables in the same query. I will have to look at the thing to find the issue. Stay tuned for an update.
Does ez_find support nested includes? This doesn't seem to work:
@names = Widget.find_with_conditions(:all, :include => [{:aaa => {:bbb => :ccc}}]) do |name, aaa, bbb, ccc| name.name =~ '%' + p[:keyword] + '%' unless p[:keyword].nil? ccc.name == p[:ccc] unless p[:c].nil? endOr is it a Rails 1.1 case?Yeah I don't think it works with the nested includes right now. The hash syntax in the nested include is confusing it. Fabien and I are working on a new release of the plugin though so we will try to have this fixed in the next release.
Do you know the release date already? :)
Site now doesn't display well (in fact quite poorly) in IE6.
I could find in the doc how to build combined OR conditions on multiple tables.. if !params[:querytext].nil? cond_b = EZ::Where::Condition.new :properties do any :properties do street =~ '%'+params[:querytext]+'%' city =~ '%'+params[:querytext]+'%' zipcode =~ '%'+params[:querytext]+'%' end any :contacts do firstName =~ '%'+params[:querytext]+'%' lastName =~ '%'+params[:querytext]+'%' end end end this gives : properties (street OR city OR zipcode) AND contacts (firstName OR lastName) how I should write it to get : properties (street OR city OR zipcode) OR contacts (firstName OR lastName) thanks for anyone help... kad
The between operator is acting a bit weird. It seems to only allow a single digit. cond = EZ::Where::Condition.new :residences do rent <=> (params[:rent]) end Parameters: {"bedrooms"=>"", "x"=>"81", "y"=>"19", "action"=>"quicksearch", "bathrooms"=>"", "controller"=>"residences", "rent"=>"400..500"} [4;36;1mResidence Load (0.000399)[0m [0;1mSELECT * FROM residences WHERE ((residences.rent BETWEEN '4' AND '0')) [0m
@Tom
You are not passing a ruby range there you are passing a single string. the <=> operator takes a ruby range or array andx not a string.
Does anyone have a code snippet that overwrites a operator to perform case insensitive searches? Something like: Operator foo =i 'bar' #=> ["lower(foo) = lower(?)", 'bar'] foo =~i '%bar' #=> ["lower(foo) like lower(?)", '%bar'] This might make no sense at all because I'm just a ROR newbie.
Mark- that is already built into the latest ez_where. Make sure to grab the ez-where package from rubyforge svn. Then you can do this:
Hi all, I'm using twice the select_date function in my view to select a date from and a date until for a date property in my model called "written_on".
<%= select_date nil, :prefix => "written_from", :order => [:day, :month, :year], :include_blank => true %>
<%= select_date Date.today, :prefix => "written_until", :order => [:day, :month, :year], :include_blank => true %>
In my model, I want to create a date range if the user has selected valid values for both the date written_from and the date written_until. The POST parameters for written_from and written_until look like this:
Parameters: {"commit"=>"Find", "written_until"=>{"month"=>"1", "day"=>"20", "year"=>"2007"}, "action"=>"find_proposals", "controller"=>"proposals", "proposal"=>{"client_firstname"=>"", "client_surname"=>"", "city"=>"", "box"=>"", "postal_code"=>"", "number"=>"", "units"=>"", "policy_number"=>"", "street"=>"", "agent_id"=>""}, "written_from"=>{"month"=>"1", "day"=>"2", "year"=>"2007"}}
I tried to create the dates like this:
written_from = Date.civil(p[:written_from][:year], p[:written_from][:mon], p[:written_from][:day])
but that doesn't seem to work. The ez_where condition that I want would look something like:written_until = Date.civil(p[:written_until][:year], p[:written_until][:mon], p[:written_until][:day])
cond = Condition.new do written_on <=> (date_from..date_until) unless date_from.nil? || date_until.nil? end
This is probably more of a ruby question, but how do I dynamically refer to the column name, such as column_name='last_name' value='Smith' conditions = EZ::Where::Condition.new do column_name == value end (I don't want to set 'colum_name == value', I want to say 'last_name == value' but I dont want to hard code in 'last_name')
Predicted values in template editing can be done through any PHP script?