1

Given a person ActiveRecord instance: person.phones #=> {home: '00123', office: '+1-45'}

Is there a more Ruby/Rails idiomatic way to do the following:

person_phones = person.phones
person_phones[:home] = person_phones[:home].sub('001', '+1')
person.update_column :phones, person_phones

The example data is irrelevant.

I only want to sub one specific hash key value and the new hash to be saved in the database. I was wondering if there was a way to do this just calling person.phones once, and not multiple times

Nacho
  • 959
  • 1
  • 6
  • 10
  • What do you mean with idiomatic? Can't you do `person.update_column(:phones, person.phones[:home].sub('001', '+1'))`? – Sebastián Palma Nov 15 '19 at 10:25
  • Hi Sebastian! In your example `sub` will return a string, and will replace the whole hash when `update_column`. So you can't do that :( – Nacho Nov 15 '19 at 10:34
  • Ah right! Really didn't see that. What RDBMS and version are you using (if any)? What datatype is phones? – Sebastián Palma Nov 15 '19 at 10:39
  • MYSQL. I know I should not be saving hashes into the database, but it's a legacy decision already implanted :) – Nacho Nov 15 '19 at 10:43
  • `person.phones.class #=> ActiveSupport::HashWithIndifferentAccess`, if that helps you. – Nacho Nov 15 '19 at 10:44
  • In that case, the more I can think is using merge, which just makes you save one line: `person.update_column(:phones, person_phones.merge(home: person_phones[:home].sub('001', '+1')))` – Sebastián Palma Nov 15 '19 at 10:51
  • Maybe ActiveRecord's `serialize` method would help you here. https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html – anothermh Nov 15 '19 at 17:07
  • Don't use serialize - its for text columns. You should probably be able to modify the JSON column in place in the database. I haven't done this with MySQL but you can maybe adapt the answer from [this question](https://stackoverflow.com/questions/34986223/how-to-update-json-data-type-column-in-mysql-5-7-10) and use it with `.update_all` instead of doing it one record at a time. Definitely worthwhile to investigate if you have a large table. – max Nov 15 '19 at 18:23
  • @max I don't see a clear response that it is a JSON column. Are we sure it is? (haven't used MySQL in a few years now) – anothermh Nov 15 '19 at 19:29
  • @anothermh Maybe I was wrong in assuming it. MySQL has had JSON columns for some years but it could also be string column used with serialize. If it is serialized as JSON at least you can use MySQLs JSON functions to parse it and do the replacement in SQL. – max Nov 15 '19 at 19:48

2 Answers2

2

Without changing much behaviour:

person.phones[:home].sub!('001', '+1')
person.save

There are a few important differences here:

  1. You modify the string object by using sub! instead of sub. Meaning that all other variables/objects that hold a reference to the string will also change.
  2. I'm using save instead of update_column. This means callbacks will not be skipped and all changes are saved instead of only the phones attribute.

From the comment I make out you're looking for a one liner, which isn't mutch different from the above:

person.tap { |person| person.phones[:home].sub!('001', '+1') }.save
3limin4t0r
  • 19,353
  • 2
  • 31
  • 52
0

You can use the before_validation callback on your model.

Like this:

class Phone < ApplicationRecord

 validates :home, US_PHONE_REGEX

 before_validation :normalize_number

private

 def normalize_number
  home.gsub!(/^001/, '+1')
 end
end

Note: I haven't tested this code, it's meant to show an approach only.

If you're looking to normalize also an international number, evaluate if the use of a lib like phony wouldn't make more sense, or the rails lib https://github.com/joost/phony_rails based on it.

EDIT since the comment clarify you only want to change the values of the hash in one like you can use Ruby's method transform_values!:

phones.transform_values!{|v| v.gsub(/^001/, '+1')}

Paulo Fidalgo
  • 21,709
  • 7
  • 99
  • 115
  • Hi Paulo! The example data is irrelevant. I just want to know if this could be done in one line, without having to call `person.phones`so many times – Nacho Nov 15 '19 at 11:19
  • Thanks Paulo! Just two minor comments: 1) I only want to `sub` one specific hash key, not all. 2) I want the change to be saved in the database. I was wondering if there was a way to do this just calling `person.phones` once, and not multiple times. Thanks again! – Nacho Nov 15 '19 at 11:38