Rails OR condition inside where with same input - ruby-on-rails

I have the following active record query:
User.where(["id = ? OR token = ?", params[:id], params[:id]]).first
Here, params[:id] = 9MrEflgr
PROBLEM
As per logic, params[:id] can be numeric id or alphanumeric token.
I want to get something like User.find_by_id_or_token(params[:id]) in where clause.
Here, since the params[:id] starts with '9', so active record gives me user with id 9 instead of checking for token field. How to avoid this?

As the comment mentioned, you need to check if the params is an integer. This SO question has good suggestions on how to do that (where you can implement is_integer? below).
if params[:id].is_integer?
User.where(["id = ? OR token = ?", params[:id], params[:id]]).first
else
User.where(["token = ?", params[:id]]).first
end

Related

Ruby on Rails beginner question : equality

I'm starting to know ROR and I was doing a kind of blog with articles, etc...
I did this code :
def show
id = params[:id]
list = Article.all
is_valid = false
list.all.each do |article|
if article.id == id
#is_valid = true
break
end
end
As you can see, this code just wants to check if the article ID exists or not. So I'm testing equality between id and article.id (which's a model linked to the appropriated table in the database) BUT when I try to use or display #is_valid boolean I saw that article.id == id is FALSE every time, even if article.id = 2 and id = 2. I tried to think about everything that can make this occuring, but I admit I still misunderstand this.
Then I ask you if you know why this is occuring. Of course, an equality like 2 == 2 will change #is_valid to true.
Thank you for your help !
Maybe its because params[:id] it's a string and article.id it's an Integer
(byebug) params
{"controller"=>"admin/my_controller", "action"=>"edit", "id"=>"1"}
And yes it is... "id" is a string "1", so you may try this:
def show
id = params[:id].to_i
list = Article.all
is_valid = false
list.all.each do |article|
if article.id == id
#is_valid = true
break
end
end
end
And maybe could work.
This is the answer to your question,
But if you want to learn a little more about Activerecord you can do this
Article.exists?(params[:id])
and that will do what you are trying to do just with a query against db.
and if you want to get just a simple article
record = Article.find_by(id: params[:id]) #return nil when not exist
if record # if nil will threat like false on ruby
#my code when exist
else
#my code when not exist
end
will work (you also can use find but find will throw an exception ActiveRecord::RecordNotFound when not exists so you have to catch that exception.
Activerecord has many ways to check this you dont need to do it by hand.
def show
#article = Article.find(params[:id])
end
This will create a database query which returns a single row. .find raises a ActiveRecord::NotFound exception if the record is not found. Rails catches this error and shows a 404 page. Article.find_by(id: params[:id]) is the "safe" alternative that does not raise.
Your code is problematic since list = Article.all will load all the records out of the database which is slow and will exhaust the memory on the server if you have enough articles. Its the least effective way possible to solve the task.
If you want to just test for existence use .exists? or .any?. This creates a COUNT query instead of selecting the rows.
Article.where(title: 'Hello World').exists?

ActiveRecord how to use Where only if the parameter you're querying has been passed?

I'm running a query like the below:
Item.where("created_at >=?", Time.parse(params[:created_at])).where(status_id: params[:status_id])
...where the user can decide to NOT provide a parameter, in which case it should be excluded from the query entirely. For example, if the user decides to not pass a created_at and not submit it, I want to run the following:
Item.where(status_id: params[:status_id])
I was thinking even if you had a try statement like Time.try(:parse, params[:created_at]), if params[created_at] were empty, then the query would be .where(created_at >= ?", nil) which would NOT be the intent at all. Same thing with params[:status_id], if the user just didn't pass it, you'd have a query that's .where(status_id:nil) which is again not appropriate, because that's a valid query in itself!
I suppose you can write code like this:
if params[:created_at].present?
#items = Item.where("created_at >= ?", Time.parse(params[:created_at])
end
if params[:status_id].present?
#items = #items.where(status_id: params[:status_id])
end
However, this is less efficient with multiple db calls, and I'm trying to be more efficient. Just wondering if possible.
def index
#products = Product.where(nil) # creates an anonymous scope
#products = #products.status(params[:status]) if params[:status].present?
#products = #products.location(params[:location]) if params[:location].present?
#products = #products.starts_with(params[:starts_with]) if params[:starts_with].present?
end
You can do something like this. Rails is smart in order to identify when it need to build query ;)
You might be interested in checking this blog It was very useful for me and can also be for you.
If you read #where documentation, you can see option to pass nil to where clause.
blank condition :
If the condition is any blank-ish object, then #where is a no-op and returns the current relation.
This gives us option to pass conditions if valid or just return nil will produce previous relation itself.
#items = Item.where(status_condition).where(created_at_condition)
private
def status_condition
['status = ?', params[:status]] unless params[:status].blank?
end
def created_at_condition
['created_at >= ?', Time.parse(params[:created_at])] unless params[:created_at].blank?
end
This would be another option to achieve the desired result. Hope this helps !

Ruby passing parameters

What is the proper way to pass the parameters to a function?
For example:
def self.find_by_example(username, email)
user = User.find_by_username(username) || User.find_by_email(email)
end
I would like to find the user by his username or email but if a create a function passing the 2 parameters Rails shows
(wrong number of arguments (given 1, expected 2))
When I call User.find_by_example('example')
I still don't get it, the parameters passed in must not be the attribute?
and why does it say "given 1"?
You must be calling the function like `User.find_by_example("name to find") and the function expects two arguments (name and email). You could define the function as:
def self.find_by_example(term)
user = User.find_by_username(term) || User.find_by_email(term)
end
And call it User.find_by_example("Name to find") or User.find_by_example("email#to_find.com")
This does not work ok if you have users with a username like an email. And it is not much efficient if you wish to search by other fields. SO you could also:
def self.find_by_example(terms)
if terms[:email]
user = User.find_by_email(terms[:email])
elsif terms[:username]
user = User.find_by_username(terms[:username])
elsif terms[:id]
user = User.find_by_id(terms[:id])
elsif terms[:document]
user = User.find_by_document(terms[:document])
end
end
And call the method User.find_by_example(:email => "email#example.com"). This is similar to the find_by method that Active Record already provides (but allows many arguments), so no need to implement it.
The proposed and accepted answer is not really the equivalent of the code asked in the question. It is accepted, so one might assume that it guessed the OP intent correctly. But I think it can be useful for (especially junior) programmers to think about the problem more deeply.
Think of what method should do
(not just if it immediately gives you result you wish to see, there can be surprises in the edge cases)
The original code
def self.find_by_example(username, email)
user = User.find_by_username(username) || User.find_by_email(email)
end
Could be used this way x.find_by_example(nil, 'test#example.com').
If we assume there can't be users with NULL username (which IMO is a reasonable assumption), the call would result in finding an user strictly by email.
The proposed solution does not give you this possibility:
def self.find_by_example(term)
user = User.find_by_username(term) || User.find_by_email(term)
end
x.find_by_example('test#example.com') would return user with such username if exists, and (possibly other) user with such e-mail otherwise.
In other words - you have less control which field is used to find a user (which can be correct, if that's really what you need)
So it depends on the OP intent.
If one want to retain how the original method works, but improve the interface, it could be done like this:
def self.find_by_example2(username: nil, email: nil)
user = User.find_by_username(username) || User.find_by_email(email)
end
And calling x.find_by_example2(email: 'test#example.com') is equivalent to x.find_by_example(nil, 'test#example.com') but looks better.
Bonus: Performance implications
The proposed solution
def self.find_by_example(term)
user = User.find_by_username(term) || User.find_by_email(term)
end
makes second query when the user is not find by username. You can improve it as well if you wish to employ some sql magic:
def self.find_by_example(term)
user = User.where("username = ? OR (username IS NULL and email = ?)", term, term).first
end
There's another possibility (though not 100% equivalent to the accepted solution):
def self.find_by_example(term)
user = User.where("username = ? OR email = ?", term, term).first
end
(I'll leave as an exercise the answer how those are different, to keep this post short...ish)
Bonus 2: flexibility
This
def self.find_by_example(terms)
if terms[:email]
user = User.find_by_email(terms[:email])
elsif terms[:username]
user = User.find_by_username(terms[:username])
elsif terms[:id]
user = User.find_by_id(terms[:id])
elsif terms[:document]
user = User.find_by_document(terms[:document])
end
end
is a waste of your time, because rails gives you already better interface to do this.
Instead of calling
x.find_by_example(document: 'foo')
you could just do
User.find_by(document: 'foo')
There's really no need to implement it that way, it's basically crippled version of ActiveRecord interface, that you have to maintain as you add new fields to User model.

Rails: update existing has_many through record via controller?

So two thirds of this works. Every time a User reads an Article, a History record is created (has_many through), which just says "User read Article at Read_Date_X".
The database is ok, the models are ok, the read_date param is permitted in the History controller, and the following operation works both 1) to check if a User has read an article before and 2) to create a new History record if it is the first time on this article.
But I cannot work out why the middle bit (to just update the read_date on an existing record) is not working. It doesn't matter if I try it with h.save! or h.update().
h = History.where(article_id: #article, user_id: current_user)
if h.exists?
h = History.where(article_id: #article, user_id: current_user)
h.read_date = Time.now
h.save!
else
h = History.new
h.article_id = #article.id
h.user_id = current_user.id
h.read_date = Time.now
h.save!
end
The error it throws if it finds an existing record is:
undefined method `read_date=' for #<History::ActiveRecord_Relation:0x007fe7f30a5e50>
UPDATE: working answer
So Derek was right, and this version works. The middle bit needed a single instance, not an array, which is what the top conditional (without .first) was checking for. Using that to return a single record, though, means you need to swap "exists?" to "present?" in the second part.
h = History.where(article_id: #article, user_id: current_user).first
if h.present?
h.read_date = Time.now
h.save!
else
h = History.new
h.article_id = #article.id
h.user_id = current_user.id
h.read_date = Time.now
h.save!
end
History.where(article_id: #article, user_id: current_user) is returning a History::ActiveRecord_Relation. If you want to set the read_date, you'll want to get a single record.
Here's one way you could do this with what you have currently:
h = History.where(article_id: #article, user_id: current_user).first
Another way you could handle this is by using find_by instead of where. This would return a single record. Like this:
h = History.find_by(article_id: #article, user_id: current_user)
However, if it's possible for a user to have many history records for an article, I would stick to the way you're doing things and make one change. If for some reason you have a lot of history records, this may not be very efficient though.
histories = History.where(article_id: #article, user_id: current_user)
histories.each { |history| history.update(read_date: Time.now) }
I realize this question is already answered. Here are a couple of additional thoughts and suggestions.
I would not have a separate read_date attribute. Just use updated_at instead. It's already there for you. And, the way your code works, read_date and updated_at will always be (essentially) the same.
When looking up whether the history exists, you can do current_user.histories.where(article: #article). IMO, that seems cleaner than: History.where(article_id: #article, user_id: current_user).first.
You can avoid all that exists? and present? business by just checking if the h assignment was successful. Thus, if h = current_user.histories.where(article: #article).
If you go the route of using updated_at instead of read_date, then you can set updated_at to Time.now by simply doing h.touch.
I would use the << method provided by has_many :through (instead of building the history record by hand). Again, if you use updated_at instead of read_date, then you can use this approach.
So, you could boil your code down to:
if h = current_user.histories.where(article: #article)
h.touch
else
current_user.articles << #article
end
You could use a ternary operator instead of that if then else, in which case it might look something like:
current_user.histories.where(article: #article).tap do |h|
h ? h.touch : current_user.articles << #article
end

Lookup by slug and id

I currently have the following in my products controller show action:
#product = Spree::Product.active.where(slug: params[:id]).first!
This only works when slug is passed, I want to be able to pass in id or slug and for it to work:
kinda like:
Spree::Product.active.where(slug: params[:id]).first! || Spree::Product.active.where(id: params[:id]).first!
Right now when I type products/foo-bar it works, not when I type products/1
Is there a way to do this? Thank you in advance.
.first!
Raises error when record not found.
Try
Spree::Product.active.find_by(slug: params[:id]) || Spree::Product.active.find_by(id: params[:id])
It'll try second query if first does not return value
The simplest way would be to search for both in a where like so:
Spree::Product.active.where("slug = :parameter OR id = :parameter", parameter: params[:id]).first!
This will find the Spree::Product where either the slug or id is equal to the params[:id].

Resources