Rails console not running multiple lines of code - ruby-on-rails

So when I write this in my server rails console:
irb(main):001:0> Member.all do |member|
irb(main):002:1* member.identify
irb(main):003:1> end
It doesn't run the identify function, it just returns this:
Member Load (1.7ms) SELECT "members".* FROM "members" ORDER BY members.created_at DESC
...
Is there something I'm doing wrong syntax wise? How do you run multiline code in a server console?

If you want to apply the .identify SQL function to Member, you don't need a multiline statement with do and end, you can just write:
Member.identify
Member.all will return an array of objects that go with the query. You typically do not need to iterate over them, as it makes more sense to write a separate query that would automatically give you what you wanted . You can put a separate function in your Model (either a class method or scope) that would give you the desired results that you want.
i.e.:
Member model:
self.some_method
where(":member_name => "Jack")
end
and from here you can call it in your views or controllers by writing #member = Member.some_method
For the future if you wanted to iterate over each element within a collection (array, hash) you need to chain the object/class to an iterator such as each, or map. An iterator is basically a method that will go over each element of your collection, and apply certain methods/operations to each item in the collection:
i.e.:
some object = [1,2,3]
some_object.each do |x|
puts x
#will go over each element in array and print it
end

You should try using the rails console (inside the app directory):
rails c
or
rails console
There you multiline will work.

Related

ActiveRecord .where() method ignored

I've got a weird issue... In a model User, that has no relation with the model Interest, I try to call this query:
# file model.rb
def self.my_func
Interest.where('id IN (?)', [1,2])
end
But it's completely ignored... and if I replace by this:
# file model.rb
def self.my_func
Interest.find(1)
end
the .find() method is triggered and I get result.
If I directly call Interest.where('id IN (?)', [1,2]) in rails console it works...
I'm on RoR 3.2.13
Any idea?
Thank you all.
Rails doesn't evaluate the query until you actually try to access the results. Calling Model.where just returns an ActiveRecord::Relation onto which you can chain additional where/order/etc calls.
Try this (.all forces the query to be evaluated and returns an array of results):
def self.my_func
Interest.where(id: [1,2]).all
end
Note that should shouldn't actually do this. It's much better for your model to just allow the ActiveRecord::Relation to be returned from the method, so the calling code can apply additional scoping/ordering methods to it.
Also note that, instead of manually building an id in (?) query, Rails is smart enough to do it for you if you just use where(id: [1,2]).
Bonus notes:
On the terminal, the results get evaluated from Model.where immediately because IRB invokes inspect on the result of each expression you enter so it has something to print, and the returned ActiveRecord::Relation evaluates its query when inspected. You can bypass this to prove the point to yourself by adding ;nil, so that your statement evaluates to nil; note that the SELECT doesn't happen until I manually call x.inspect:
irb(main):008:0> x = User.where("name like 'bob'"); nil
=> nil
irb(main):009:0> x.inspect
User Load (0.5ms) SELECT "users".* FROM "users" WHERE (name like 'bob')
=> "[]"

Are the .order method parameters in ActiveRecord sanitized by default?

I'm trying to pass a string into the .order method, such as
Item.order(orderBy)
I was wondering if orderBy gets sanitized by default and if not, what would be the best way to sanitize it.
The order does not get sanitized. This query will actually drop the Users table:
Post.order("title; drop table users;")
You'll want to check the orderBy variable before running the query if there's any way orderBy could be tainted from user input. Something like this could work:
items = Item.scoped
if Item.column_names.include?(orderBy)
items = items.order(orderBy)
end
They are not sanitized in the same way as a .where clause with ?, but you can use #sanitize_sql_for_order:
sanitize_sql_for_order(["field(id, ?)", [1,3,2]])
# => "field(id, 1,3,2)"
sanitize_sql_for_order("id ASC")
# => "id ASC"
http://api.rubyonrails.org/classes/ActiveRecord/Sanitization/ClassMethods.html#method-i-sanitize_sql_for_order
Just to update this for Rails 5+, as of this writing, passing an array into order will (attempt to) sanitize the right side inputs:
Item.order(['?', "'; DROP TABLE items;--"])
#=> SELECT * FROM items ORDER BY '''; DROP TABLE items;--'
This will trigger a deprecation warning in Rails 5.1 about a "Dangerous query method" that will be disallowed in Rails 6. If you know the left hand input is safe, wrapping it in an Arel.sql call will silence the warning and, presumably, still be valid in Rails 6.
Item.order([Arel.sql('?'), "'; DROP TABLE items;--"])
#=> SELECT * FROM items ORDER BY '''; DROP TABLE items;--'
It's important to note that unsafe SQL on the left side will be sent to the database unmodified. Exercise caution!
If you know your input is going to be an attribute of your model, you can pass the arguments as a hash:
Item.order(column_name => sort_direction)
In this form, ActiveRecord will complain if the column name is not valid for the model or if the sort direction is not valid.
I use something like the following:
#scoped = #scoped.order Entity.send(:sanitize_sql, "#{#c} #{#d}")
Where Entity is the model class.
Extend ActiveRecord::Relation with sanitized_order.
Taking Dylan's lead I decided to extend ActiveRecord::Relation in order to add a chainable method that will automatically sanitize the order params that are passed to it.
Here's how you call it:
Item.sanitized_order( params[:order_by], params[:order_direction] )
And here's how you extend ActiveRecord::Relation to add it:
config/initializers/sanitized_order.rb
class ActiveRecord::Relation
# This will sanitize the column and direction of the order.
# Should always be used when taking these params from GET.
#
def sanitized_order( column, direction = nil )
direction ||= "ASC"
raise "Column value of #{column} not permitted." unless self.klass.column_names.include?( column.to_s )
raise "Direction value of #{direction} not permitted." unless [ "ASC", "DESC" ].include?( direction.upcase )
self.order( "#{column} #{direction}" )
end
end
It does two main things:
It ensures that the column parameter is the name of a column name of the base klass of the ActiveRecord::Relation.
In our above example, it would ensure params[:order_by] is one of Item's columns.
It ensures that the direction value is either "ASC" or "DESC".
It can probably be taken further but I find the ease of use and DRYness very useful in practice when accepting sorting params from users.

How to found in Rails using a method that is not an attribute

How do you make the following work...
Beagle.beagles.where(:snount => "short")
...in situations where a snout method id defined for Beagle, but there is no snout column in the beagles table?
If you need a scope (i.e. an ActiveRecord::Relation object) then I think you're out of luck, because where deals with DB query logic, not model instance methods. But if you just need to get an array of results that satisfy the method requirement, then this will work:
Beagle.select { |b| b.snout == "short" }
Note that this will grab all records from the DB, then select the ones you want from them. Here's the generated SQL:
Beagle Load (0.1ms) SELECT "beagles".* FROM "beagles"
See also: instance method in scope
maybe try this
beagles = Beagle.find(:all)
beagles.each do |beagle|
if beagle.snount == "short"
beagles_selected << beagle
end
end
you will then have a beagles_selected array containing all the beagles you need

Rails 3.0 with "or" statement in each loop

I am trying to build a query that compares two object, and if they have the same id the record is not fetched. What I have is this:
#channels.each do |channel|
unless #guide_channels.where(:id => channel.id).exists?
#dropdown_channels = #dropdown_channels.where(:id => channel.id)
end
end
This creates the query, but puts a AND between every value which isn't what I have in mind. I want the "or" operator. Is there a 'orwhere' function I can use or is there a better way to do this with some compare function?
The point is that the .where() method of a AR::Relation objects adds the condition to a set of conditions that are then AND-ed together when the query is executed.
What you have to do is a query like NOT IN:
# select all the ids for related #guide_channels
# if #channels comes from a query (so it's a ActiveRecord::Relation):
guide_ids = #guide_channels.where(:id => #channels.pluck(:id)).pluck(:id)
# use .where(:id => #channels.map {|c| c.id}) instead if #channels is just an array
# then use the list to exclude results from the following.
#dropdown_channels = #dropdown_channels.where("id NOT IN (?)", guide_ids.to_a)
The first query will accumulate all the IDs for channels that have an entry in #guide_channels. The second one will use the result of the first to exclude the found channels from tthe results for the dropdown.
This strange behavior is due to ActiveRecord's lazy evaluation of scopes.
What happens is that the line
#dropdown_channels = #dropdown_channels.where(:id => channel.id)
Does not send a query to the DB until you actually use the value of #dropdown_channels, and when you do it concatenates all the conditions into one big query, this is why you get the AND between the conditions.
In order to force ActiveRecord to eager load the scope you can use either the all scope or the first scope, for example:
#dropdown_channels = #dropdown_channels.where(:id => channel.id).first
This will force ActiveRecord to calculate the query on spot returning the result immediately and not to accumulate the scopes for lazy evaluation.
Another approach could be to accumulate all those channels_ids and get them later in one query, instead of making a query for each one. This approach is more cost-effective relating to DB resources.
In order to achieve this :
dropdown_channels_ids = []
#channels.each do |channel|
unless #guide_channels.where(:id => channel.id).exists?
dropdown_channels_ids << channel.id
end
end
#dropdown_channels = #dropdown_channels.where(:id => dropdown_channels_ids)

Rails 3 - Expression-based Attribute in Model

How do I define a model attribute as an expression of another attribute?
Example:
Class Home < ActiveRecord::Base
attr_accessible :address, :phone_number
Now I want to be able to return an attribute like :area_code, which would be an sql expression like "substr(phone_number, 1,3)".
I also want to be able to use the expression / attribute in a group by query for a report.
This seems to perform the query, but does not return an object with named attributes, so how do I use it in a view?
Rails Console:
#ac = Home.group("substr(phone_number, 1,3)").count
=> #<OrderedHash {"307"=>3, "515"=>1}>
I also expected this to work, but not sure what kind of object it is returning:
#test = Home.select("substr(phone_number, 1,3) as area_code, count(*) as c").group("substr(phone_number, 1,3)")
=> [#<Home>, #<Home>]
To expand on the last example. Here it is with Active Record logging turned on:
>Home.select("substr(phone_number, 1,3) as area_code, count(*) as c").group("substr(phone_number, 1,3)")
Output:
Home Load (0.3ms) SELECT substr(phone_number, 1,3) as area_code, count(*) as c FROM "homes" GROUP BY substr(phone_number, 1,3)
=> [#<Home>, #<Home>]
So it is executing the query I want, but giving me an unexpected data object. Shouldn't I get something like this?
[ #<area_code: "307", c: 3>, #<area_code: "515", c: 1> ]
you cannot access to substr(...) because it is not an attribute of the initialized record object.
See : http://guides.rubyonrails.org/active_record_querying.html "selecting specific fields"
you can workaround this this way :
#test = Home.select("substr(phone_number, 1,3) as phone_number").group(:phone_number)
... but some might find it a bit hackish. Moreover, when you use select, the records will be read-only, so be careful.
if you need the count, just add .count at the end of the chain, but you will get a hash as you already had. But isn't that all you need ? what is your purpose ?
You can also use an area_code column that will be filled using callbacks on create and update, so you can index this column ; your query will run fast on read, though it will be slower on insertion.

Resources