I've been playing around Rails (4) + Postgres JSON fields a bit now, and I've noticed that if I do something like this
model.json_data = {
field1: "hello",
field2: "world"
}
model.save
it works fine. However if I do
model.update_column(:json_data, {
field1: "hello",
field2: "world"
} )
it doesn't work. It doesn't seem like update_column is storing the data as JSON, but just a string with line breaks and tabs included. The problem is, I want the json_data to be generated in an after_save callback, so I need to not re-trigger the after_save callback when updating the JSON field.
Any thoughts on what might be going on here, or how to get around it?
Nevermind, I found a solution.
model.update_column(:json_data, {
field1: "hello",
field2: "world"
}.to_json )
Seems obvious in hindsight.
Related
I have a new API attribute which is an array of hashes and I would like to validate it as part of built-in rails validation. Since this is a complex object I'm validating I am not finding any valid examples to refer from.
The parameter name is books which are an array of hashes and each hash has three properties genre which should be an enum of three possible values and authors which should be an array of integers and bookId which should be an integer.
Something like this books: [{bookId: 4, genre: "crime", authors: [2, 3, 4]}]
If it's something like an array I can see the documentation for it https://guides.rubyonrails.org/active_record_validations.html here but I am not finding any examples of the above scenarios.
I'm using rails 4.2.1 with ruby 2.3.7 it would be great if you could help me with somewhere to start with this.
For specifically, enum validation I did find a good answer here How do I validate members of an array field?. The trouble is when I need to use this in an array of hashes.
You can write a simple custom validation method yourself. Something like this might be a good start:
validate :format_of_books_array
def format_of_books_array
unless books.is_a?(Array) && books.all? { |b| b.is_a?(Hash) }
errors.add(:books, "is not an array of hashes")
return
end
errors.add(:books, "not all bookIds are integers") unless books.all? { |b| b[:bookId].is_a?(Integer) }
errors.add(:books, "includes invalid genres") unless books.all? { |b| b[:genre].in?(%w[crime romance thriller fantasy]) }
errors.add(:books, "includes invalid author array") unless books.all? { |b| b[:authors].is_a?(Array) && b[:authors].all? { |a| a.is_a?(Integer) } }
end
Perhaps my understanding of how this is supposed to work is wrong, but I seeing strings stored in my DB when I would expect them to be a jsonb array. Here is how I have things setup:
Migration
t.jsonb :variables, array: true
Model
attribute :variables, :variable, array: true
Custom ActiveRecord::Type
ActiveRecord::Type.register(:variable, Variable::Type)
Custom Variable Type
class Variable::Type < ActiveRecord::Type::Json
include ActiveModel::Type::Helpers::Mutable
# Type casts a value from user input (e.g. from a setter). This value may be a string from the form builder, or a ruby object passed to a setter. There is currently no way to differentiate between which source it came from.
# - value: The raw input, as provided to the attribute setter.
def cast(value)
unless value.nil?
value = Variable.new(value) if !value.kind_of?(Variable)
value
end
end
# Converts a value from database input to the appropriate ruby type. The return value of this method will be returned from ActiveRecord::AttributeMethods::Read#read_attribute. The default implementation just calls #cast.
# - value: The raw input, as provided from the database.
def deserialize(value)
unless value.nil?
value = super if value.kind_of?(String)
value = Variable.new(value) if value.kind_of?(Hash)
value
end
end
So this method does work from the application's perspective. I can set the value as variables = [Variable.new, Variable.new] and it correctly stores in the DB, and retrieves back as an array of [Variable, Variable].
What concerns me, and the root of this question, is that in the database, the variable is stored using double escaped strings rather than json objects:
{
"{\"token\": \"a\", \"value\": 1, \"default_value\": 1}",
"{\"token\": \"b\", \"value\": 2, \"default_value\": 2}"
}
I would expect them to be stored something more resembling a json object like this:
{
{"token": "a", "value": 1, "default_value": 1},
{"token": "b", "value": 2, "default_value": 2}
}
The reason for this is that, from my understanding, future querying on this column directly from the DB will be faster/easier if in a json format, rather than a string format. Querying through rails would remain unaffected.
How can I get my Postgres DB to store the array of jsonb properly through rails?
So it turns out that the Rails 5 attribute api is not perfect yet (and not well documented), and the Postgres array support was causing some problems, at least with the way I wanted to use it. I used the same approach to the problem for the solution, but rather than telling rails to use an array of my custom type, I am using a custom type array. Code speaks louder than words:
Migration
t.jsonb :variables, default: []
Model
attribute :variables, :variable_array, default: []
Custom ActiveRecord::Type
ActiveRecord::Type.register(:variable_array, VariableArrayType)
Custom Variable Type
class VariableArrayType < ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Jsonb
def deserialize(value)
value = super # turns raw json string into array of hashes
if value.kind_of? Array
value.map {|h| Variable.new(h)} # turns array of hashes into array of Variables
else
value
end
end
end
And now, as expected, the db entry is no longer stored as a string, but rather as searchable/indexable jsonb. The whole reason for this song and dance is that I can set the variables attribute using plain old ruby objects...
template.variables = [Variable.new(token: "a", default_value: 1), Variable.new(token: "b", default_value: 2)]
...then have it serialized as its jsonb representation in the DB...
[
{"token": "a", "default_value": 1},
{"token": "b", "default_value": 2}
]
...but more importantly, automatically deserialized and rehydrated back into the plain old ruby object, ready for me to interact with it.
Template.find(123).variables = [#<Variable:0x87654321 token: "a", default_value: 1>, #<Variable:0x12345678 token: "b", default_value: 2>]
Using the old serialize api causes a write with every save (intentionally by Rails architectural design), regardless of whether or not the serialized attribute had changed. Doing this all manually by overriding setters/getters is an unnecessary complication due to the numerous ways attributes can be assigned, and is partly the reason for the newer attributes api.
If it helps anyone else, Rails wants you to provide the possible keys to permit in the controller as well if you're using strong params:
def controller_params
params.require(:parent_key)
.permit(
jsonb_field: [:allowed_key1, :allowed_key2, :allowed_key3]
)
end
One solution could be to just parse the variable via JSON.parse, push it inside an empty array, then assign it to the attribute.
variables = []
variable = "{\"token\": \"a\", \"value\": 1, \"default_value\": 1}"
variable.class #String
parsed_variable = JSON.parse(variable) #{"token"=>"a", "value"=>1, "default_value"=>1}
parsed_variable.class #Hash
variables.push parsed_variable
I have a string params, whose value is "1" or "['1','2','3','4']". By using eval method, I can get the result 1 or [1,2,3,4], but I need the result [1] or [1,2,3,4].
params[:city_id] = eval(params[:city_id])
scope :city, -> (params) { params[:city_id].present? ? where(city_id: (params[:city_id].is_a?(String) ? eval(params[:city_id]) : params[:city_id])) : all }
Here i don't want eval.
scope :city, -> (params) { params[:city_id].present? ? where(city_id: params[:city_id]) : all }
params[:city_id] #should be array values e.g [1] or [1,2,3,4] instead of string
Your strings look very close to JSON, so probably the safest thing you can do is parse the string as JSON. In fact:
JSON.parse("1") => 1
JSON.parse('["1","2","3","4"]') => ["1","2","3","4"]
Now your array uses single quotes. So I would suggest you to do:
Array(JSON.parse(string.gsub("'", '"'))).map(&:to_i)
So, replace the single quotes with doubles, parse as JSON, make sure it's wrapped in an array and convert possible strings in the array to integers.
I've left a comment for what would be my preferred approach: it's unusual to get your params through as you are, and the ideal approach would be to address this. Using eval is definitely a no go - there are some big security concerns to doing so (e.g. imagine someone submitting "City.delete_all" as the param).
As a solution to your immediate problem, you can do this using a regex, scanning for digits:
str = "['1','2','3','4']"
str.scan(/\d+/)
# => ["1", "2", "3"]
str = '1'
str.scan(/\d+/)
# => ["1"]
# In your case:
params[:city_id].scan(/\d+/)
In very simple terms, this looks through the given string for any digits that are in there. Here's a simple Regex101 with results / an explanation: https://regex101.com/r/41yw9C/1.
Rails should take care of converting the fields in your subsequent query (where(city_id: params[:city_id])), though if you explictly want an array of integers, you can append the following (thanks #SergioTulentsev):
params[:city_id].scan(/\d+/).map(&:to_i)
# or in a single loop, though slightly less readable:
[].tap { |result| str.scan(/\d+/) { |match| result << match.to_i } }
# => [1, 2, 3, 4]
Hope that's useful, let me know how you get on or if you have any questions.
I am working on an app in Rails 4 using i18n-active_record 0.1.0 to keep my translations in the database rather than in a .yml-file. It works fine.
One thing that I am struggling with, however, is that each translation record is one record per locale, i.e.
#1. { locale: "en", key: "hello", value: "hello")
#2. { locale: "se", key: "hello", value: "hej")
which makes updating them a tedious effort. I would like instead to have it as one, i.e.:
{ key: "hello", value_en: "hello", value_se: "hej" }
or similar in order to update all instances of one key in one form. I can't seem to find anything about that, which puzzles me.
Is there any way to easily do this? Any type of hacks would be ok as well.
You could make an ActiveRecord object for the translation table, and then create read and write functions on that model.
Read function would pull all associated records then combine them into a single hash.
Write function would take your single hash input and split them into multiple records for writing/updating.
I ended up creating my own Translation functionality using Globalize. It does not explicitly rely on I18n so it is a parallell system but it works, although not pretty and it is not a replacement to I18n but it has the important functionality of being able to easily add a locale and handle all translations in one form.
Translation model with key:string
In Translation model:
translates :value
globalize_accessors :locales => I18n.available_locales, :attributes => [:value]
In ApplicationHelper:
def t2(key_str)
key_stringified = key_str.to_s.gsub(":", "")
t = Transl8er.find_by_key(key_stringified)
if t.blank?
# Translation missing
if t.is_a? String
return_string = "Translation missing for #{key_str}"
else
return_string = key_str
end
else
begin
return_string = t.value.strip
rescue
return_string = t.value
end
end
return_string
end
I'm trying to send a JSON API response from my rails app to Dasheroo which expects the following format:
{
my_statistic: { type: 'integer', value: 1, label: 'My Statistic' }
}
However it is not happy with my data structure generated by the following code:
In controller:
def count_foo_members
#foo = Foo.all.count
end
In count_foo_members.json.jbuilder:
json.foo_members do
json.type 'integer'
json.value #foo
json.label 'Foo Members'
end
If I open this route in my browser I can see the following:
{
"foo_members":{"type":"integer","value":1,"label":"Foo Members"}
}
From the results above, the only thing that I can see that could have an effect on the result is the fact that my JSON result has quotation marks around the JSON Key values.
My question thus is: How can I remove these quotation marks in Rails 4 and JBuilder?
JSON.parse(you_response) and you get standart hash.
You cannot remove the quotation marks from the keys. The responsibility is on the consumer (Dasheroo) to parse your JSON string into a JavaScript Object, which will "remove" the quotes from the keys.
Read json-object-with-or-without-quotes for further practical insight.