In rails 4, I have created an enum called books which is
enum books_category: {
horror: 1,
mystery: 2,
drama: 3
}
I have a column in the database called Books where I populated it using a csv file. My database attributes are name and books_category. Now when I upgraded to rails 5, books_category attribute of database is directly mapping to the string instead of integer. For example, previously books_category consists of 1 after upgrading it's saving the string "horror". How do I solve this issue?
You can use two ways.
1) create a model which saves business_categories in it.
or
2) when you retrieve record which is storing horror, you can pass it to the model like
my_model = Model.find_by("business_categot = ?","horror")
Model.books_category[my_model.books_category] # Returns the integer value
First of all, you are missing a colon after books_category. The code should look like:
enum books_category: {
horror: 1,
mystery: 2,
drama: 3
}
Please edit your question to include the datatype of books_category attribute.
You can change the value either using strings or ints
book = Book.last
book.books_category = 1
book.save!
=> book.books_category = "horror"
book = Book.last
book.books_category = 'drama'
book.save!
=> book.books_category = "drama"
Please note that Rails always returns the string value even though they are saved in integer datatype.
You can get the hash of all books_categories by calling
Book.book_categories
=> {"horror"=>1, "mystery"=>2, "drama"=>3}
And the keys and values by calling
Book.book_categories.keys
=> ["horror", "mystery", "drama"]
and
Book.book_categories.values
=> [1, 2, 3]
Finally, you can get a list of all the objects(books) having the category of horror with
Book.horror
=> Book Load (0.1ms) SELECT "books".* FROM "books" WHERE "books"."books_category" = ? [["books_category", 1]]
As you can observe from the query, it uses integer to search through the Books.
If you really want the values of your enum, consider using Enumerize gem. It is simpler and offers much more flexibility
Further Reading: https://hackhands.com/ruby-on-enums-queries-and-rails-4-1/
Related
I have a table which contains an integer based column (status) which I'm using for an enum attribute within the Rails model.
At the moment of doing:
Post.select(:id, ..., :status)
Defined as:
enum status: { inactive: 0, active: 1, ... }
It returns everything as expected, but the status column is returned in its string value as inactive, active, etc. But I need it as an integer.
How can I get that?
I'm currently just using ActiveRecord::Base.connection.execute and passing a raw query:
ActiveRecord::Base.connection.execute('select id, ..., status from posts')
It won't be the most elegant way but this is the only way I see possible without looping over your objects.
You can achieve this by creating 2 classes like this:
class Post < ApplicationRecord
end
And the other one:
class PostSave < Post
enum status: { inactive: 0, active: 1, ... }
end
With this when you use Post class you won't get enum value for the "status" column but when you use PostSave class you can use your enum as you were already using.
Now when you do
Post.select(:id, :status)
It will give you integer values for the "status" column as you desire.
Yep. And that's simply how enum field works in Rails. Data is stored as integer, but displayed as string - based on what's defined in enum:
https://api.rubyonrails.org/v5.2.3/classes/ActiveRecord/Enum.html
If you really want to get integer try one of those:
Remove enum declaration. Data is stored as Integer. Without that line query will return Integer
Thats not the most beautiful code but: Post.statuses[post.status] will work
Have you tried this?
# Rails < 5
post = Post.find(123)
post.read_attribute(:status)
# Rails >= 5
post = Post.find(123)
post.read_attribute_before_type_cast(:status)
You can give this a try:
Post.select(:id, ..., 'status as int_status')
I'm not sure this directly applies as I do not know Rails or your exact requirement. But you indicated the desire to "get a collection of records from the DB"; so perhaps it will.
Postgres contains 2 tables in pg_catalog you need to join to get the collection values for a enum: pg_type and pg_enum as follows (where ENUM_NAME is replaced by the appropriate enum type name:
select t.typname,e.enumsortorder, e.enumlabel
from pg_type t
join pg_enum e
on (t.oid = e.enumtypid)
where t.typname = 'ENUM_NAME'
order by e.enumsortorder;
One difference I've notice is that in Postgres the associated numeric value is defined as Real, not as Integer. This is due to how the values are actually maintained. Example:
create type t_ord_stat as enum ('Ordered', 'Delivered', 'Billed', 'Complete', 'Canceled');
select e.enumsortorder, e.enumlabel
from pg_type t
join pg_enum e
on (t.oid = e.enumtypid)
where t.typname = 't_ord_stat'
order by e.enumsortorder;
alter type t_ord_stat add value 'Suppended: Out-of-Stock' after 'Ordered';
alter type t_ord_stat add value 'Suppended: Credit Check' before 'Suppended: Out-of-Stock' ;
alter type t_ord_stat add value 'Suppended: Customer Refused Delivery' before 'Billed';
select e.enumsortorder, e.enumlabel
from pg_type t
join pg_enum e
on (t.oid = e.enumtypid)
where t.typname = 't_ord_stat'
order by e.enumsortorder;
The above gives the actual numeric values underlying the enum string value (as column name indicates it's for sorting purpose). You could get a relative integer value with
select row_number() over() relative_seq, en.enumlabel
from (select e.enumsortorder, e.enumlabel
from pg_type t
join pg_enum e
on (t.oid = e.enumtypid)
where t.typname = 't_ord_stat'
order by e.enumsortorder) en;
I'll leave the actual conversion to Rails to you. Hope it helps.
You could map the column in the SQL statement to another field and read it from there instead, that way Rails passes the value directly as it doesn't know it's an enum (it doesn't know the column at all):
Post.select('id, title, status, status AS status_int').each do |post|
puts post.status # 'inactive'
puts post.status_int # 0
end
If you HAVE to have the same column name, then you're out of luck unless you do a little bit more work:
class Post
STATUSES = %w(inactive active)
end
Post.all.each do |post|
index = Post::STATUSES.index(post.status)
# use 'index' here instead of `post.status`
end
If neither of those are sufficient then this most certainly sounds like the http://xyproblem.info/ ... as these answers should work for 99% of cases. So you should probably explain WHY you cant just use status_int while working with the objects, or why you can't store the int in a variable, or... why you need access to the integer at all, which defeats the purpose of an enum.
The above solutions are working. I have tried with raw sql on mysql version 5.7.28.
the below query is working with mysql only.
I am still looking query for postgres, I will let you know soon.
create table Posts(id integer, title varchar(100), status ENUM ('active', 'inactive') NOT NULL);
insert into Posts(id, title, status) values(1, "Hello", 'inactive')
select title, status+0 from Posts;
I created an array in a rails runner(this is not a model and has no attributes) like:
name_1 = 5
name_2 = 14
name_3 = 26
name_4 = 3
...
#names = [name_1, name_2, name_3, name_4, name_5]
Each "name_x" has an integer stored as its value.
How can I order the array so that it orders the output [highest => lowest] by the current values, and also shows the "name" [key, value] ?.
Currently,
puts #names
shows only the values with no order. Tks for pointing me in the right direction, theres many posts relating arrays but most asume its a model with attributes where you can say to order by the attribute. how do you order when you have no specific attributes like in this scenario?
If you want names (keys) and values, use a Hash.
#names = {name_1: 5, name_2: 14, name_3: 26, name_4: 3}
A Hash is Enumerable, so you can sort it. However sorting results in an array, but its easy to make that a Hash again, and hashes in Ruby maintain the order of insertion.
#names.sort_by{|k,v| v}
[[:name_4, 3], [:name_1, 5], [:name_2, 14], [:name_3, 26]]
#names.sort_by{|k,v| v}.to_h
{:name_4=>3, :name_1=>5, :name_2=>14, :name_3=>26}
The natural sort order is ascending (smallest to largest), but you can just negate the sort_by value, or reverse the resulting array.
#names.sort_by{|k,v| -v}.to_h
{:name_3=>26, :name_2=>14, :name_1=>5, :name_4=>3}
#names.sort_by{|k,v| v}.reverse.to_h
{:name_3=>26, :name_2=>14, :name_1=>5, :name_4=>3}
I'm beginning to learn RoR, but i've a problem which i don't understand. With Product.find :all returns all the records from DB. But if i want to find_by_gender(1) (or even 2) it returns a nil, i'm certain that the db contains products with a gender
My code controller:
gender = params[:gender].to_i
#search_results = Product.find_by_gender(gender)
this returns a nill,
What am i doing wrong?
Greetings!
find_by_... returns either first record or nil if none found, find_all_by_... returns all records that match (or empty array if none). In your case nil means no records found with gender = 1.
Verify your data first!
Look at some sample records:
Do something like:
Product.all(:limit => 5).each {|product| product.id.to_s + product.gender}
or go into sql
sql> select id, gender from products where id < 6;
If you are to verify what the gender values are you can then create named scopes in your model for those conditions, e.g. (rails3)
(Product Model - app/models/product.rb)
scope :male where(:gender) = male_value # i.e. 1 or 'M' or 'Male' or whatever
scope :female where(:gender) = female_value # i.e. '2' or 'F' or whatever
Which will then you let write Products.male or Products.female !
Final note - should gender be in your users table? , or is this for male / female specific products?
in rails console execute
Product.pluck(:gender)
And u will know that values does it have in AR(i think true and false), so u have to use query Product.find_by_gender(true)
I am new to rails. What I see that there are a lot of ways to find a record:
find_by_<columnname>(<columnvalue>)
find(:first, :conditions => { <columnname> => <columnvalue> }
where(<columnname> => <columnvalue>).first
And it looks like all of them end up generating exactly the same SQL. Also, I believe the same is true for finding multiple records:
find_all_by_<columnname>(<columnvalue>)
find(:all, :conditions => { <columnname> => <columnvalue> }
where(<columnname> => <columnvalue>)
Is there a rule of thumb or recommendation on which one to use?
where returns ActiveRecord::Relation
Now take a look at find_by implementation:
def find_by
where(*args).take
end
As you can see find_by is the same as where but it returns only one record. This method should be used for getting 1 record and where should be used for getting all records with some conditions.
Edit:
This answer is very old and other, better answers have come up since this post was made. I'd advise looking at the one posted below by #Hossam Khamis for more details.
Use whichever one you feel suits your needs best.
The find method is usually used to retrieve a row by ID:
Model.find(1)
It's worth noting that find will throw an exception if the item is not found by the attribute that you supply. Use where (as described below, which will return an empty array if the attribute is not found) to avoid an exception being thrown.
Other uses of find are usually replaced with things like this:
Model.all
Model.first
find_by is used as a helper when you're searching for information within a column, and it maps to such with naming conventions. For instance, if you have a column named name in your database, you'd use the following syntax:
Model.find_by(name: "Bob")
.where is more of a catch all that lets you use a bit more complex logic for when the conventional helpers won't do, and it returns an array of items that match your conditions (or an empty array otherwise).
Model.find
1- Parameter: ID of the object to find.
2- If found: It returns the object (One object only).
3- If not found: raises an ActiveRecord::RecordNotFound exception.
Model.find_by
1- Parameter: key/value
Example:
User.find_by name: 'John', email: 'john#doe.com'
2- If found: It returns the object.
3- If not found: returns nil.
Note: If you want it to raise ActiveRecord::RecordNotFound use find_by!
Model.where
1- Parameter: same as find_by
2- If found: It returns ActiveRecord::Relation containing one or more records matching the parameters.
3- If not found: It return an Empty ActiveRecord::Relation.
There is a difference between find and find_by in that find will return an error if not found, whereas find_by will return null.
Sometimes it is easier to read if you have a method like find_by email: "haha", as opposed to .where(email: some_params).first.
Since Rails 4 you can do:
User.find_by(name: 'Bob')
which is the equivalent find_by_name in Rails 3.
Use #where when #find and #find_by are not enough.
The accepted answer generally covers it all, but I'd like to add something,
just incase you are planning to work with the model in a way like updating, and you are retrieving a single record(whose id you do not know), Then find_by is the way to go, because it retrieves the record and does not put it in an array
irb(main):037:0> #kit = Kit.find_by(number: "3456")
Kit Load (0.9ms) SELECT "kits".* FROM "kits" WHERE "kits"."number" =
'3456' LIMIT 1
=> #<Kit id: 1, number: "3456", created_at: "2015-05-12 06:10:56",
updated_at: "2015-05-12 06:10:56", job_id: nil>
irb(main):038:0> #kit.update(job_id: 2)
(0.2ms) BEGIN Kit Exists (0.4ms) SELECT 1 AS one FROM "kits" WHERE
("kits"."number" = '3456' AND "kits"."id" != 1) LIMIT 1 SQL (0.5ms)
UPDATE "kits" SET "job_id" = $1, "updated_at" = $2 WHERE "kits"."id" =
1 [["job_id", 2], ["updated_at", Tue, 12 May 2015 07:16:58 UTC +00:00]]
(0.6ms) COMMIT => true
but if you use where then you can not update it directly
irb(main):039:0> #kit = Kit.where(number: "3456")
Kit Load (1.2ms) SELECT "kits".* FROM "kits" WHERE "kits"."number" =
'3456' => #<ActiveRecord::Relation [#<Kit id: 1, number: "3456",
created_at: "2015-05-12 06:10:56", updated_at: "2015-05-12 07:16:58",
job_id: 2>]>
irb(main):040:0> #kit.update(job_id: 3)
ArgumentError: wrong number of arguments (1 for 2)
in such a case you would have to specify it like this
irb(main):043:0> #kit[0].update(job_id: 3)
(0.2ms) BEGIN Kit Exists (0.6ms) SELECT 1 AS one FROM "kits" WHERE
("kits"."number" = '3456' AND "kits"."id" != 1) LIMIT 1 SQL (0.6ms)
UPDATE "kits" SET "job_id" = $1, "updated_at" = $2 WHERE "kits"."id" = 1
[["job_id", 3], ["updated_at", Tue, 12 May 2015 07:28:04 UTC +00:00]]
(0.5ms) COMMIT => true
Apart from accepted answer, following is also valid
Model.find() can accept array of ids, and will return all records which matches.
Model.find_by_id(123) also accept array but will only process first id value present in array
Model.find([1,2,3])
Model.find_by_id([1,2,3])
The answers given so far are all OK.
However, one interesting difference is that Model.find searches by id; if found, it returns a Model object (just a single record) but throws an ActiveRecord::RecordNotFound otherwise.
Model.find_by is very similar to Model.find and lets you search any column or group of columns in your database but it returns nil if no record matches the search.
Model.where on the other hand returns a Model::ActiveRecord_Relation object which is just like an array containing all the records that match the search. If no record was found, it returns an empty Model::ActiveRecord_Relation object.
I hope these would help you in deciding which to use at any point in time.
Suppose I have a model User
User.find(id)
Returns a row where primary key = id. The return type will be User object.
User.find_by(email:"abc#xyz.com")
Returns first row with matching attribute or email in this case. Return type will be User object again.
Note :- User.find_by(email: "abc#xyz.com") is similar to User.find_by_email("abc#xyz.com")
User.where(project_id:1)
Returns all users in users table where attribute matches.
Here return type will be ActiveRecord::Relation object. ActiveRecord::Relation class includes Ruby's Enumerable module so you can use it's object like an array and traverse on it.
Both #2s in your lists are being deprecated. You can still use find(params[:id]) though.
Generally, where() works in most situations.
Here's a great post: https://web.archive.org/web/20150206131559/http://m.onkey.org/active-record-query-interface
The best part of working with any open source technology is that you can inspect length and breadth of it.
Checkout this link
find_by ~> Finds the first record matching the specified conditions. There is no implied ordering so if order matters, you should specify it yourself. If no record is found, returns nil.
find ~> Finds the first record matching the specified conditions , but if no record is found, it raises an exception but that is done deliberately.
Do checkout the above link, it has all the explanation and use cases for the following two functions.
I will personally recommend using
where(< columnname> => < columnvalue>)
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.