Rename a certain key in a hash - ruby-on-rails

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".

Related

how to pull all images from a hash field?

I have an Activity_Part model with a field called activity_json. this field is a hash that has a key named "image". I want to query/select only the ID of all Activity_Part only with activity_json field with key "image"
ActivityPart < ApplicationRecord {
:id => :integer,
:owner_id => :integer,
:activity_type => :string,
:activity_json => :json,
I thought of doing something like this:
x = ActivityPart.all
x.pluck(activity_json: 'image')
but I don't know how to return just the Activity ID with all the images in the activty_json field
I'll assume that you're using postgres.
jsonb type has the ? operator which can be used like this: ActivityPart.where("activity_json ? 'image'") as per documentation.
If your column is of a json type then consider changing it to jsonb. You can alternatively cast it to jsonb in the query like so: ActivityPart.where("activity_json::jsonb ? 'image'") but be aware that this might negatively affect the performance of the query.

ruby comparison of time and variable

I have two models, being an Employee and a WorkingPattern. An instance of an Employee belongs_to an Working Pattern.
The Working Pattern looks like this
:id => :integer,
:name => :string,
:mon => :boolean,
:tue => :boolean,
:wed => :boolean,
:thu => :boolean,
:fri => :boolean,
:sat => :boolean,
:sun => :boolean
I need to know if an Employee should be at work today. So, if today is a Tuesday and that employee's working pattern record reports that :tue = true then return true, etc. I don't have the option of renaming the fields on the WorkingPattern model to match the days names.
I know that
Time.now.strftime("%A")
will return the name of the day. Then I figured out I can get the first 3 characters of the string by doing
Time.now.strftime("%A")[0,3]
so now I have "Tue" returned as a string. Add in a downcase
Time.now.strftime("%A")[0,3].downcase
and now I have "tue", which matches the symbol for :tue on the WorkingPattern.
Now I need a way of checking the string against the correct day, ideally in a manner that doesn't mean 7 queries against the database for each employee!
Can anyone advise?
You can use %a for the abbreviated weekday name. And use send to dynamically invoke a method
employee.working_pattern.send(Time.now.strftime("%a").downcase)
Use send to invoke a method, stored in a variable, on an object.
Both of these are identical:
user.tue # true
user.send('tue') # true
You can access an attibute using [], just like a Hash. No need to use send or even attributes.
day = Time.now.strftime("%a").downcase
employee.working_pattern[day]
Butchering strings makes me feel a little uneasy
Construct a hash of day numbers:
{0 => :sun
1 => :mon,
2 => :tue,
...}
Then use Time.now.wday (or Time.zone.now.wday if you want to be timezone aware) to select the appropriate value from the hash
You can then use that string on employee.working_pattern in any of the ways described by the other answers:
employee.working_pattern.send(day_name)
employee.working_pattern.read_attribute[day_name]
enployee.working_pattern[day_name]

Rails: Serialize value as comma-separated and not as YAML

I'm looking for a way to store a serialized value of eg. IDs in a column. In before claims that this is not an optimal design: the column is used for IDs of associated records, but will only be used when displaying the record - so no queries are made with selection on the column and no joins will be made on this column either.
In Rails I can serialize the column by using:
class Activity
serialize :data
end
This encodes the column as YAML. For legacy sake and since I'm only storing one dimensional arrays containing only integers, I find it more suitable to store it as a comma-separated value.
I've successfully implemented basic accessors like this:
def data=(ids)
ids = ids.join(",") if ids.is_a?(Array)
write_attribute(:data, ids)
end
def data
(read_attribute(:data) || "").split(",")
end
This works pretty fine. However I'd like to add array-like methods to this attribute:
activity = Activity.first
activity.data << 42
...
How would I do this?
You can do it with composed_of feature as explained in this post.
It should be something like:
composed_of :data, :class_name => 'Array', :mapping => %w(data to_csv),
:constructor => Proc.new {|column| column.to_csv},
:converter => Proc.new {|column| column.to_csv}
after_validation do |u|
u.data = u.data if u.data.dirty? # Force to serialize
end
Haven't tested it though.
You can use serialize with a custom coder in rails 3.1.
See my answer to this question. :-)

How to coerce type of ActiveRecord attribute returned by :select phrase on joined table?

Having trouble with AR 2.3.5, e.g.:
users = User.all( :select => "u.id, c.user_id", :from => "users u, connections c",
:conditions => ... )
Returns, e.g.:
=> [#<User id: 1000>]
>> users.first.attributes
=> {"id"=>1000, "user_id"=>"1000"}
Note that AR returns the id of the model searched as numeric but the selected user_id of the joined model as a String, although both are int(11) in the database schema.
How could I better form this type of query to select columns of tables backing multiple models and retrieving their natural type rather than String ? Seems like AR is punting on this somewhere. How could I coerce the returned types at AR load time and not have to tack .to_i (etc.) onto every post-hoc access?
It's unfortunately not going to happen very easily. All of the data from the DB connection comes to rails as strings, the conversion of types happens in each of the dynamic attribute methods that rails creates at runtime. It knows which attributes to convert to which type by the table's column-type meta-data that it retrieves when the app starts. Each model only has column meta-data for it's own columns, that's why it's own columns end up with correct type. There is no easy way to auto-convert to the correct types.
You could on the other hand, create a simple conversion method that would take a Hash and automatically convert the attributes.
Something like this:
users = User.all(:select => "cl, comments.c2", ...)
users = convert_columns(users, 'c2' => :integer, 'other_column' => :date)
def convert_columns(records, columns = {})
records.each do |rec|
columns.each do |col, type|
rec[col] = case type
when :int then rec[col].to_i
when :date then ........
....
end
end
end
end
Why are you using :from => "users" inside a User.method ?
The following will do an inner join (which is what you are doing anyways)
users = User.all(:include => :connections, :select => "users.id, connections.user_id", :conditions => {...})
This is going to be very heavy query for the database.
Faster query would be with the outer join though.
This will also return the keys as INT not STRING
A much faster alternative was
Connection.all(:include => :user, :conditions => {...}).collect {|e| [e.user_id, e.id] }
This gives you an array of arrays with the ids. If you are going to select "id, user_id" columns only, then it may not necessarily be as AR object. An array can be faster.
I hope I am not missing some point here. Suggest me, if I am.
If you want quick solution - try to use after_find callback and preset correct attributes types there:
class User < ActiveRecord::Base
after_find :preset_types
private
def preset_types user
user.user_id = user.user_id.to_i
end
end

(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