ruby map find_each can't add to array - ruby-on-rails

I have 2 Models: Document and Keywords. They are habtm in relation to each other and they both accepts_nested_attributes_for each other so I can create a nested form. That works well.
So in params, I have
"document"=>{"book_id"=>"1", "keywords"=>{"keywords"=>"term, administration, witness "}, ...
In the controller I put the keywords in a separate array like this :
q = params[:document][:keywords].fetch(:keywords).split(",")
This works well too.
What I now need to do, is get the keywords ids and put them in an array. Each element of that array will populate the join table.
I've tried this :
a = Array.new
q.each do |var|
id = Keyword.select(:id).find_by keyword: var
a << id
id
end
But, this only answers [#<Keyword id: 496>, nil, nil], although the server log shows that all 3 SQL requests are executed and they are correct.
I have also tried this :
a = Array.new
q.map do |e|
Keyword.where(motcle: e).select(:id).find_each do |wrd|
a << wrd.id
end
end
Then again, this only return the FIRST id of the keyword, although the server log shows that all 3 SQL requests are executed.
What I'm trying to get is a = [496, 367, 2398]
So I have 2 questions :
1/ Why are the ids not added to the array, despite the server executing all SQL requests ?
2/ How to write in rails a request would be
SELECT "motclefs"."id" FROM "motclefs" WHERE "motclefs"."motcle" in ('déchéances','comtesse') ORDER BY "motclefs"."id";
Thanks !

The returned value is an object of Keyword. You need to get the id attribute of the object. eg:
id = Keyword.where(keyword: var).select(:id).first.id
A better way to get all the ids would be
a = Keyword.where(keyword: ['term', 'administration', 'witness']).pluck(:id)
# I think this might answer your second question.

Related

rails : how can I use find_each with map?

I have a form that has nested field (habtm and accepts_nested_attributes_for). That form contains with a field "keywords", that autocompletes keywords that come from a postgresql table.
All that works well. This is in params :
"acte"=>{"biblio_id"=>"1", "keywords"=>{"keywords"=>"judge, ordeal, "}
What I now need to do is take those keywords and get their keywords_id out of the table keywords. Those id must be added to the join table.
I'm doing this :
q = params[:acte][:keywords].fetch(:keywords).split(",")
a = q.map {|e| Keyword.find_by keyword: e }
As per the guides, find_by returns only the first matching field. I guess I would need to use find_each but I'm not certain about that and I can't get it to word. I have tried this:
q = params[:acte][:motclefs].fetch(:motclefs).split(",")
a = Array.new
Motclef.where(motcle: q).find_each do |mot|
a << mot.id
end
This also finds only the first result like : [251].
What I'm looking to get is something like [1453, 252, 654]
thanks !
Putting find_by in a loop means you will be executing a separate SQL query for each SQL keyword.
You can instead just get all the ids in a single SQL call by doing keyword in.
After you do q = params[:acte][:keywords].fetch(:keywords).split(","), your q will be an array of keywords. So q will be ["judge", " ordeal"].
You can simply do Keyword.where(keyword: q).select(:id) which will generate a query like SELECT keywords.id FROM keywords where keyword in ('judge', 'ordeal').

Copy/Clone objects from database

I have relations as follows :
Customer has many Orders
Order has many Parts
What I'm trying to do is to assign orders/parts to another customer, but I want to keep the original customer/orders/parts relation. So I tried this (assuming customer is a Customer instance)
another_customer_id = 22
customer.orders.each do |order|
ord = order.dup
ord.customer_id = another_customer_id
ord.save!
order.parts.each do |part|
prt = part.dup
prt.order_id = ord.id
prt.save!
end
end
So I'm assuming once I do customer_two.orders I will get everything as in first customer, but strangely I don't. It seems the first customer has double the orders/parts.
How can I do this in a better way?
update
When I use .dup method the original customers orders are doubled in the database.
When I use .clone instead of .dup nothing happens, another customer still doesn't have orders from original customer
The code you have here looks reasonable to me if you are using Rails >= 3.1. Before Rails 3.1 you need to use clone instead of dup. However, here is another method that you can try:
another_customer_id = 22
customer.orders.each do |order|
ord = Order.create(order.attributes.merge(:customer_id => another_customer_id))
order.parts.each do |part|
Part.create(part.attributes.merge(:order_id => ord.id))
end
end

Postgres ORDER BY values in IN list using Rails Active Record

I receive a list of UserIds(about 1000 at a time) sorted by 'Income'. I have User records in "my system's database" but the 'Income' column is not there. I want to retrieve the Users from "my system's database"
in the Sorted Order as received in the list. I tried doing the following using Active Record expecting that the records would be retrieved in the same order as in the Sorted List but it does not work.
//PSEUDO CODE
User.all(:conditions => {:id => [SORTED LIST]})
I found an answer to a similar question at the link below, but am not sure how to implement the suggested solution using Active Record.
ORDER BY the IN value list
Is there any other way to do it?
Please guide.
Shardul.
Your linked to answer provides exactly what you need, you just need to code it in Ruby in a flexible manner.
Something like this:
class User
def self.find_as_sorted(ids)
values = []
ids.each_with_index do |id, index|
values << "(#{id}, #{index + 1})"
end
relation = self.joins("JOIN (VALUES #{values.join(",")}) as x (id, ordering) ON #{table_name}.id = x.id")
relation = relation.order('x.ordering')
relation
end
end
In fact you could easily put that in a module and mixin it into any ActiveRecord classes that need it, since it uses table_name and self its not implemented with any specific class names.
MySQL users can do this via the FIELD function but Postgres lacks it. However this questions has work arounds: Simulating MySQL's ORDER BY FIELD() in Postgresql

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.

Rails, how to sanitize SQL in find_by_sql

Is there a way to sanitize sql in rails method find_by_sql?
I've tried this solution:
Ruby on Rails: How to sanitize a string for SQL when not using find?
But it fails at
Model.execute_sql("Update users set active = 0 where id = 2")
It throws an error, but sql code is executed and the user with ID 2 now has a disabled account.
Simple find_by_sql also does not work:
Model.find_by_sql("UPDATE user set active = 0 where id = 1")
# => code executed, user with id 1 have now ban
Edit:
Well my client requested to make that function (select by sql) in admin panel to make some complex query(joins, special conditions etc). So I really want to find_by_sql that.
Second Edit:
I want to achieve that 'evil' SQL code won't be executed.
In admin panel you can type query -> Update users set admin = true where id = 232 and I want to block any UPDATE / DROP / ALTER SQL command.
Just want to know, that here you can ONLY execute SELECT.
After some attempts I conclude sanitize_sql_array unfortunatelly don't do that.
Is there a way to do that in Rails??
Sorry for the confusion..
Try this:
connect = ActiveRecord::Base.connection();
connect.execute(ActiveRecord::Base.send(:sanitize_sql_array, "your string"))
You can save it in variable and use for your purposes.
I made a little snippet for this that you can put in initializers.
class ActiveRecord::Base
def self.escape_sql(array)
self.send(:sanitize_sql_array, array)
end
end
Right now you can escape your query with this:
query = User.escape_sql(["Update users set active = ? where id = ?", true, params[:id]])
And you can call the query any way you like:
users = User.find_by_sql(query)
Slightly more general-purpose:
class ActiveRecord::Base
def self.escape_sql(clause, *rest)
self.send(:sanitize_sql_array, rest.empty? ? clause : ([clause] + rest))
end
end
This one lets you call it just like you'd type in a where clause, without extra brackets, and using either array-style ? or hash-style interpolations.
User.find_by_sql(["SELECT * FROM users WHERE (name = ?)", params])
Source: http://blog.endpoint.com/2012/10/dont-sleep-on-rails-3-sql-injection.html
Though this example is for INSERT query, one can use similar approach for UPDATE queries. Raw SQL bulk insert:
users_places = []
users_values = []
timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
params[:users].each do |user|
users_places << "(?,?,?,?)" # Append to array
users_values << user[:name] << user[:punch_line] << timestamp << timestamp
end
bulk_insert_users_sql_arr = ["INSERT INTO users (name, punch_line, created_at, updated_at) VALUES #{users_places.join(", ")}"] + users_values
begin
sql = ActiveRecord::Base.send(:sanitize_sql_array, bulk_insert_users_sql_arr)
ActiveRecord::Base.connection.execute(sql)
rescue
"something went wrong with the bulk insert sql query"
end
Here is the reference to sanitize_sql_array method in ActiveRecord::Base, it generates the proper query string by escaping the single quotes in the strings. For example the punch_line "Don't let them get you down" will become "Don\'t let them get you down".
I prefer to do it with key parameters. In your case it may looks like this:
Model.find_by_sql(["UPDATE user set active = :active where id = :id", active: 0, id: 1])
Pay attention, that you pass ONLY ONE parameter to :find_by_sql method - its an array, which contains two elements: string query and hash with params (since its our favourite Ruby, you can omit the curly brackets).

Resources