For array of ActiveRecord objects, return array of their attributes - ruby-on-rails

#matched = [1, 2, 3]
Where each integer represents the id of an ActiveRecord object in the Inventory class. As a next step, I want to look at each of those objects and obtain the email of the parent User, but I'm not sure how to do it. Ideally I'd write something like:
Inventory.where(id: #matched).user.email
Because certainly, this statement would work if I only had a single id to look up. Since that doesn't work, I'm instead doing this
#email = []
#matched.each do |i|
#email << Inventory.find_by_id(i).user.email
end
Just wondering if there's an easier way.
Thanks!

If you only need the email addresses then you can use the pluck method:
Inventory.where(id: #matched).joins(:user).pluck("users.email")

class Inventory
def self.with_ids(ids)
sql = #matched.map{|id| "id = #{id}"}.join(" OR ")
where(sql)
end
def parent_email
user.email
end
end
Inventory.with_ids(#matched).map(&:parent_email)

Related

Rails how to use where method for search or return all?

I am trying to do a search with multiple attributes for Address at my Rails API.
I want to search by state, city and/or street. But user doesn't need to send all attributes, he can search only by city if he wants.
So I need something like this: if the condition exists search by condition or return all results of this condition.
Example:
search request: street = 'some street', city = '', state = ''
How can I use rails where method to return all if some condition is nil?
I was trying something like this, but I know that ||:all doesn't work, it's just to illustrate what I have in mind.:
def get_address
address = Adress.where(
state: params[:state] || :all,
city: params[:city] || :all,
street: params[:street] || :all)
end
It's possible to do something like that? Or maybe there is a better way to do it?
This is a more elegant solution using some simple hash manipulation:
def filter_addesses(scope = Adress.all)
# slice takes only the keys we want
# compact removes nil values
filters = params.permit(:state, :city, :street).to_h.compact
scope = scope.where(filters) if filters.any?
scope
end
Once you're passing a column to where, there isn't an option that means "on second thought don't filter by this". Instead, you can construct the relation progressively:
def get_address
addresses = Address.all
addresses = addresses.where(state: params[:state]) if params[:state]
addresses = addresses.where(city: params[:city]) if params[:city]
addresses = addresses.where(street: params[:street]) if params[:street]
addresses
end
I highly recommend using the Searchlight gem. It solves precisely the problem you're describing. Instead of cluttering up your controllers, pass your search params to a Searchlight class. This will DRY up your code and keep your controllers skinny too. You'll not only solve your problem, but you'll have more maintainable code too. Win-win!
So in your case, you'd make an AddressSearch class:
class AddressSearch < Searchlight::Search
# This is the starting point for any chaining we do, and it's what
# will be returned if no search options are passed.
# In this case, it's an ActiveRecord model.
def base_query
Address.all # or `.scoped` for ActiveRecord 3
end
# A search method.
def search_state
query.where(state: options[:state])
end
# Another search method.
def search_city
query.where(city: options[:city])
end
# Another search method.
def search_street
query.where(street: options[:street])
end
end
Then in your controller you just need to search by passing in your search params into the class above:
AddressSearch.new(params).results
One nice thing about this gem is that any extraneous parameters will be scrubbed automatically by Searchlight. Only the State, City, and Street params will be used.

Ignore parameters that are null in active record Rails 4

I created a simple web form where users can enter some search criteria to look for venues e.g. a price range. When a user clicks "find" I use active record to query the database. This all works very well if all fields are filled in. Problems occur when one or more fields are left open and therefore have a value of null.
How can I work around this in my controller? Should I first check whether a value is null and create a query based on that? I can imagine I end up with many different queries and a lot of code. There must be a quicker way to achieve this?
Controller:
def search
#venues = Venue.where("price >= ? AND price <= ? AND romance = ? AND firstdate = ?", params[:minPrice], params[:maxPrice], params[:romance], params[:firstdate])
end
You may want to filter out all of the blank parameters that were sent with the request.
Here is a quick and DRY solution for filtering out blank values, triggers only one query of the database, and builds the where clause with Rails' ActiveRecord ORM.
This approach safeguards against SQL-injection, as pointed out by #DanBrooking. Rails 4.0+ provides "strong parameters." You should use the feature.
class VenuesController < ActiveRecord::Base
def search
# Pass a hash to your query
#venues = Venue.where(search_params)
end
private
def search_params
params.
# Optionally, whitelist your search parameters with permit
permit(:min_price, :max_price, :romance, :first_date).
# Delete any passed params that are nil or empty string
delete_if {|key, value| value.blank? }
end
end
I would recommend to make method in Venue
def self.find_by_price(min_price, max_price)
if min_price && max_price
where("price between ? and ?", min_price, max_price)
else
all
end
end
def self.find_by_romance(romance)
if romance
where("romance = ?", romance)
else
all
end
end
def self.find_by_firstdate(firstdate)
if firstdate
where("firstdate = ?", firstdate)
else
all
end
end
And use it in your controller
Venue
.find_by_price(params[:minPrice], params[:maxPrice])
.find_by_romance(params[:romance])
.find_by_firstdate(params[:firstdate])
Another solution to this problem, and I think a more elegant one, is using scopes with conditions.
You could do something like
class Venue < ActiveRecord::Base
scope :romance, ->(genre) { where("romance = ?", genre) if genre.present? }
end
You can then chain those, which would work as an AND if there is no argument present, then it is not part of the chain.
http://guides.rubyonrails.org/active_record_querying.html#scopes
Try below code, it will ignore parameters those are not present
conditions = []
conditions << "price >= '#{params[:minPrice]}'" if params[:minPrice].present?
conditions << "price <= '#{params[:maxPrice]}'" if params[:maxPrice].present?
conditions << "romance = '#{params[:romance]}'" if params[:romance].present?
conditions << "firstdate = '#{params[:firstdate]}'" if params[:firstdate].present?
#venues = Venue.where(conditions.join(" AND "))

Rails - where with multiple like options

I am trying pull information from a contacts table based on multiple like conditions. So far I have come up with the following
conditions = ""
conditions << "email_address LIKE '%#{params[:email_address]}%'" unless params[:email_address].blank?
conditions << " AND first_name LIKE '%#{params[:first_name]}%'" unless params[:first_name].blank?
conditions << " AND last_name LIKE '%#{params[:last_name]}%'" unless params[:last_name].blank?
conditions.sub!(/^AND/, '')
if !conditions.blank?
#contacts = Contact.where(conditions).page(params[:page]).per(10)
else
#contacts = Contact.all.page(params[:page]).per(10)
end
What I was wondering is ... is this the best way to do this? I would have thought there would be a nice way to add multiple conditions in the form of a hash and somehow specify that I want to use OR/AND and like.
I am fairly new to rails and google is not really helping much.
Thanks.
Just append the where calls directly to a scope:
#contacts = Contact.scoped
#contacts = #contacts.where("email_address LIKE '%?%'", params[:email_address]) if params[:email_address].present?
#contacts = #contacts.where("first_name LIKE '%?%'", params[:first_Name]) if params[:first_name].present?
#contacts = #contacts.where("last_name LIKE '%?%'", params[:last_name]) if params[:last_name].present?
You can use a simple loop to make it less repetative:
%(email_address first_name last_name).each do |field|
#contacts = #contacts.where("#{field} like '%?%'", params[field]) if params[field].present?
end
And do not build queries by hand by directly substituting user input into your query string. Rails makes that hard to do on purpose: You're bypassing all of Rails' sanitization and opening yourself to SQL injection.
I would have thought there would be a nice way to add multiple conditions in the form of a hash and somehow specify that I want to use OR/AND and like.
There is, but it only works with AND and =:
#contacts.where(first_name: "bob", last_name: "smith")
# select ... where first_name = 'bob' and last_name = 'smith'

Rails model optimization

I have a List model below, it has a has_and_belongs_to_many association with recipients. The purpose of the method make_recipient_lists is to save a parsed csv of numbers(initial parameter) in this format [[num1],[num2],[num3]...].
add_recipients work by finding existing recipients then adding them to the list or creating new recipients.
This whole process works well for small amount, 20k of numbers in 28minutes. However, the greater the number, the longer it takes exponentially, 70k took 14hours. Probably because it was checking for duplicates to a cached current_lists.
Question is, is there any way to make this faster? I am probably approaching this problem wrong. Thanks!
class List < ActiveRecord::Base
#other methods above
def make_recipient_lists(numbers,options)
rejected_numbers = []
account = self.user.account
#caching recipients
current_recipients = self.recipients
numbers.each do |num|
add_recipient(num[0], current_recipients)
end
end
def add_recipient(num, current_recipients)
account = self.user.account
recipient = current_recipients.where(number:num, account_id: account.id).first
recipient ||= current_recipients.create!(number:num, account_id: account.id)
recipient
end
end
You could do something like this. I have not tested this, but you get the idea.
def make_recipient_lists(numbers, options)
rejected_numbers = []
account = self.user.account
existing_numbers = self.recipients.where(number: numbers, account_id: account.id).map(&:number)
new_records = (numbers - existing_numbers).map {|n| {number: n, account_id: account.id, list_id: self.id} }
Recipient.create new_records
end
I think, you should use rails active_record query interface. you can use method find_or_create method for this: It will make your queries faster. change your method like this, and check the time difference:
def make_recipient_lists(numbers,options)
rejected_numbers = []
account = self.user.account
#caching recipients
current_recipients = self.recipients
numbers.each do |num|
self.recipients.find_or_create_by(number: num, account_id: account.id)
end
end
Hope it will help. Thanks.

Can I use AR object as hash key or should I use object_id instead

Because of Ruby awesomeness it is possible to use any object as key
document = Document.find 1
o = Hash.new
o[1] = true
o[:coool] = 'it is'
o[document] = true
# an it works
o[document]
#=> true
but just because it is possible doesn't mean is good practice
However I have situation where in my controller I need to set something similar, so I can loop trough it in view
#controller
#users_with_things = Hash.new
Things.accessible_by(some_curent_user_logic).each do |thing|
#user_with_things[thing.user] ||= Array.new
#user_with_things[thing.user] << thing.id
end
#view
- #users_with_things.each do |user, thing_ids|
%input{type: :checkbox, name: "blank[user_#{user.id}]", value: 1, class: "select_groups", :'data-resource-ids' => "[#{thing_ids.join(',')}]", :'data-user-type' => user.type }
The reason why I want to do it this way is because I don't want to call from my view User.find_by_id (want to make it clean)
#controller
#users_with_things = Hash.new
Things.accessible_by(some_curent_user_logic).each do |thing|
#user_with_things[thing.user.id] ||= Array.new
#user_with_things[thing.user.id] << thing.id
end
#view
- #users_with_things.each do |user_id, thing_ids|
- user = User.find user_id
%input{type: :checkbox, name: "blank[user_#{user.id}]", value: 1, class: "select_groups", :'data-resource-ids' => "[#{thing_ids.join(',')}]", :'data-user-type' => user.type }
So my 1st question is: is it ok to use ActiveRecord object as Hash key in situation like this
I can imagine several scenarios where this may go wrong (sessions, when object changes in model and so on) however this is just for rendering in a view
Alternative !
so this is one way to do it, the other may be like this
#controller
#users_with_things = Hash.new
Things.accessible_by(some_curent_user_logic).each do |thing|
#user_with_things[thing.user.object_id] ||= Array.new
#user_with_things[thing.user.object_id] << thing.id
end
#view
- #users_with_things.each do |user_object_id, thing_ids|
- user = ObjectSpace._id2ref(user_object_id) #this will find user object from object_id
%input{type: :checkbox, name: "blank[user_#{user.id}]", value: 1, class: "select_groups", :'data-resource-ids' => "[#{thing_ids.join(',')}]"", :'data-user-type' => user.type }
...which is even more, hardcore. However it is way around if for some reason hash[ARobject] = :something would create big memory cluster for some reason
question 2 : is it good idea to do it this way ?
to be complete there is also another alternative and that is
# ...
#user_with_thing[ [thing.user.id, thing.user.type] ] << thing_id
# ...
so basically array object will be key
#user_with_thing[ [1, 'Admin'] ]
#=> [1,2,3]
I think to use a hash is a good way to organise in your situation. However, I would advise against using the user or to big an object as hash keys, simply because it renders your hash unreadable and because it is really only this sole object with it's object id that can be used as a key.
o = Object.new
h = { o => 'something' }
h[Object.new] #=> nil
In your situation, this may not be an issue, because you simply need to iterate it. But it may be a shot in the leg as soon as you want to do something else with that hash, or you have different instances of the same Active Record Data (which is very common in Rails applications, unless you are a really paying attention what gets loaded when). Besides that, I think it is good to stick by the widely used convention to use simple objects (strings, symbols) as hash keys to make your code readable and maintainable.
Maybe it would be best to keep a two-dimensional hash, like this:
#users_with_things = Things.accessible_by(some_curent_user_logic).inject({}) do |a, thing|
user_id = thing.user.id
a[user_id] ||= { :user => thing.user, :things => [] }
a[user_id][:thing] << thing
a
end
Then you can iterate over #users_with_things in your view like this:
#users_with_things.each do |user_id, values|
# values[:user] is the user, values[:things] the array of things

Resources