Hash Table Column Values as Integer - ruby-on-rails

I have followed this tutorial to have a hash column on my model which works great.
When I work out codes in the rails console, it becomes different within my controller code. In the console:
Foo.update_attributes(bar: {"a" => 1, "b" => 2})
My values are shown as an integer, in the console. In my controller, if I'm doing calculations, I have to add .to_i or .to_f and it becomes messy. My values will forever be a number. Can I simple add an int or float for the column?
add_column :foos, :bar, :hstore, :integer, default: {}, null: false
The above does not work.

You can use a hook on your model:
before_save -> r { r.bar.each{|k, v| r.bar[k] = v.to_i} }

Related

How to store enum as string to database in rails

How do I create a migration in ruby where the default is a string rather than an Integer, I want to store enum into the database, but I do not want to store it as Integer, because then it does not make sense to another application that wants to use the same table. How do I do default: "female" instead of default:0
class AddSexToUsers < ActiveRecord::Migration
def change
add_column :users, :sex, :integer, default: 0
end
end
class User < ActiveRecord::Base
enum sex: [:female, :male]
has_secure_password
end
I
Reading the enum documentation, you can see Rails use the value index of the Array explained as:
Note that when an Array is used, the implicit mapping from the values to database integers is derived from the order the values appear in the array.
But it is also stated that you can use a Hash:
it's also possible to explicitly map the relation between attribute and database integer with a Hash.
With the example:
class Conversation < ActiveRecord::Base
enum status: { active: 0, archived: 1 }
end
So I tested using Rails 4.2.4 and sqlite3 and created an User class with a string type for sex type and a Hash in the enum with string values(I am using fem and mal values to differ from female and male):
Migration:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :sex, default: 'fem'
end
end
end
Model:
class User < ActiveRecord::Base
enum sex: { female: 'fem', male: 'mal' }
end
And in console:
u = User.new
#=> #<User id: nil, sex: "fem">
u.male?
#=> false
u.female?
#=> true
u.sex
#=> "female"
u[:sex]
#=> "fem"
u.male!
# INSERT transaction...
u.sex
#=> "male"
u[:sex]
#=> "mal"
I normally do the following:
# in the migration in db/migrate/…
def self.up
add_column :works, :status, :string, null: false, default: 'offering'
end
# in app/models/work.rb
class Work < ApplicationRecord
ALL_STATES = %w[canceled offering running payment rating done].freeze
enum status: ALL_STATES.zip(ALL_STATES).to_h
end
By using a hash as argument for enum (see docs) this stores strings in the database. At the same time this still allows you to use all the cool Rails helper methods:
w = Work.new
#=> #<Work id: nil, status: "offering">
w.rating?
#=> false
w.offering?
#=> true
w.status
#=> "offering"
w[:status]
#=> "offering"
w.done!
# INSERT transaction...
w.status
#=> "done"
w[:status]
#=> "done"
Update for one-liner:
I overlooked completely we've got index_by since Rails 1.2.6. This makes the solution a one-liner even:
enum status: %w[canceled offering running payment rating done].index_by(&:to_sym)
Alternatively we've got index_with since Rails 6.0.0:
enum status: %i[canceled offering running payment rating done].index_with(&:to_s)
enum in Rails and ENUM type in MySQL are 2 different things.
enum in Rails is just a wrapper around your integer column so it's easier for you to use strings in queries, rather than integers. But on database level it's all converted to integers (automatically by Rails), since that's the type of the column.
ENUM type in MySQL is vendor-specific column type (for example, SQLite doesn't support it, but PostgreSQL does). In MySQL :
An ENUM is a string object with a value chosen from a list of permitted values that are enumerated explicitly in the column specification at table creation time.
CREATE TABLE shirts (
name VARCHAR(40),
size ENUM('x-small', 'small', 'medium', 'large', 'x-large')
);
INSERT INTO shirts (name, size) VALUES ('dress shirt','large'), ('t-shirt','medium'),
('polo shirt','small');
SELECT name, size FROM shirts WHERE size = 'medium';
+---------+--------+
| name | size |
+---------+--------+
| t-shirt | medium |
+---------+--------+
For the migration, you need to do this:
class AddSexToUsers < ActiveRecord::Migration
def change
add_column :users, :sex, "ENUM('female', 'male') DEFAULT 'female'"
end
end
Take a look at this Gist, Rails doesn't provide it out of the box so you have to use a concern:
https://gist.github.com/mani47/86096220ccd06fe46f0c09306e9d382d
There's steps to add enum as string to model Company
bin/rails g migration AddStatusToCompanies status
class AddStatusToCompanies < ActiveRecord::Migration[7.0]
def change
add_column :companies, :status, :string, null: false, default: 'claimed'
add_index :companies, :status
end
end
bin/rails db:migrate
Values are strings (symbols not working)
add Default
add Prefix
enum status: {
claimed: 'claimed',
unverified: 'unverified',
verified: 'verified',
}, default: 'claimed'
Add validation (or will raise sql exception)
validates :status, inclusion: { in: statuses.keys }, allow_nil: true
To my knowledge it is not possible with standard Rails enum. Look at https://github.com/lwe/simple_enum, it is more functionally rich, and also allows storing of enum values as strings to DB (column type string, i.e. varchar in terms of DB).

Using update_attributes with column type of array

I have a languages column which is an array of strings with
add_column :table, :languages, :string, array: true, default: []
When I use update_attributes on that model, all columns are being updated except the languages column which remains []
Is there anything special needed to do here?
When you post an array onto an action in the controller, you should permit like this:
params.require(:table).permit( {:languages => []}, :other_field, :other_field2... )

Rename a certain key in a hash

I have a column car_details with 2000 entries, each of which is a hash of info that looks like this:
{"capacity"=>"0",
"wheels"=>"6",
"weight"=>"3000",
"engine_type"=>"Diesel",
"horsepower"=>"350",
"fuel_capacity"=>"35",
"fuel_consumption"=>"30"}
Some cars have more details, some have less. I want to rename the "fuel_consumption" key to "mpg" on every car that has that key.
Well, a previous answer will generate 2000 requests, but you can use the REPLACE function instead. Both MySQL and PostgreSQL have that, so it will be like:
Car.update_all("car_details = REPLACE(car_details, 'fuel_consumption', 'mpg')")
Take a look at the update_all method for the conditions.
See also PostgreSQL string functions and MySQL string functions.
Answer posted by #Ivan Shamatov works very well and is particular important to have good performances on huge databases.
I tried it with a PostgreSQL database, on a jsonb column.
To let it works we have to pay same attention to data type casting.
For example on a User model like this:
User < ActiveRecord::Base {
:id => :integer,
:created_at => :datetime,
:updated_at => :datetime,
:email => :string,
:first_name => :string,
:last_name => :string,
:custom_data => :jsonb
}
My goal was to rename a key, inside custom_data jsonb field.
For example custom_data hash content from:
{
"foo" => "bar",
"date" => "1980-07-10"
}
to:
{
"new_foo" => "bar",
"date" => "1980-07-10"
}
For all users records present into my db.
We can execute this query:
old_key = 'foo'
new_key = 'new_foo'
User.update_all("custom_data = REPLACE(custom_data::text, '#{old_key}'::text, '#{new_key}'::text)::jsonb")
This will only replace the target key (old_key), inside our jsonb hash, without changing hash values or other hash keys.
Note ::text and ::jsonb type casting!
As far as I know, there is no easy way to update a serialized column in a data table en masse with raw SQL. The best way I can think of would be to do something like:
Car.find_each do |car|
mpg = car.car_details.delete("fuel_consumption")
car.car_details["mpg"] = mpg if mpg
car.save
end
This is assuming that you are using Active Record and your model is called "Car".

Inserting a column value with null in ruby on rails

I want to customize the value inserted in the column in the controller and i want to insert a null value in it. In this way :
#users.to = null
I believe you want:
#user.to = nil
You may have to run a migration to ensure the column allows NULL values:
change_column :users, :to, :string, :null => true

(Rails) Is there a way to check the field's datatype?

How do you check what the datatype is for something that was retrieved from the database?
For example, if I have some instantiation of a model #model with a database field "title", I want to be able to code something like #model.title.type and have it return "String". Does Rails have any built-in functionality for this?
Try this:
#model.column_for_attribute('title').type
Should return :string, :text, :integer, etc.
The ActiveRecord Column class also includes a number of other attributes: default, limit, name, null, precision, primary, scale, sql_type, type.
In Rails 3, for my model "Firm", I'd use Firm.columns_hash.
Firm.columns_hash["name"].type #returns :string
If you want to iterate through them, you'd do something like this:
Firm.columns_hash.each {|k,v| puts "#{k} => #{v.type}"}
which will output the following:
id => integer
name => string
max_trade_qty => integer
and so on.

Resources