Prior to Rails 4.2.0, ActiveRecord would automatically convert a String to the type specified by serialize.
class Post < ActiveRecord::Base
serialize :options, Array # Column type: 'text', DB: PostgreSQL
end
class PostTest < ActiveSupport::TestCase
test "assign options" do
post = Post.new
post.options = "[1,2,3]" # => "[1,2,3]"
end
end
In Rails 4.2.1
class Post < ActiveRecord::Base
serialize :options, Array
end
class PostTest < ActiveSupport::TestCase
test "assign options" do
post = Post.new
post.options = "[1,2,3]" # ActiveRecord::SerializationTypeMismatch: Attribute was supposed to be a Array, but was a String.
end
end
I can't find this in the documentation, changelogs. Was this type of String to Array conversion removed or is it a bug? In my use case, I have such a String from params assigned to a model. It works in Rails 4.1.10 but in Rails 4.2.1 it raises ActiveRecord::SerializationTypeMismatch.
I'm not sure which commit between 4.1.10 and 4.2.1 introduced this behaviour but it's not a bug as it is well documented here. In particular,
If class_name is specified, the serialized object must be of that class on assignment and retrieval. Otherwise SerializationTypeMismatch will be raised.
That's strange because YAMLColumn has been behaving like this for the years. Take a look at this commit.
Related
I have a model
class Product < ApplicationRecord
translates :title
end
When I create a record I then use globalize3 to create translations. My question is: how do I get the original untranslated record value of :title ?
Because when I do Product.last.title.translations I get all translation values, and when I do Product.last.title I only get the translation of the current I18n value.
I'm assuming you're using globalize and not globalize3 which has not been updated in almost 10 years. If not you should switch.
Also the whole idea of "original untranslated value" is kind of nonsense when dealing with Globalize. All the values are stored on the translations table.
You can switch locales by using Globalize.with_locale:
class Product < ApplicationRecord
translates :title
def original_title
Globalize.with_locale(I18n.default_locale) do
title
end
end
end
This assumes that records are originally created in the default locale. If that is not the case you could query the translations table:
class Product < ApplicationRecord
translates :title
def original_title
translations.first.title
end
end
I have a hash store as follows:
store :order_details, accessors: %w{item_name, external_id, total......etc}, coder: JSON
I'm deserializing this to a ruby object using a lib class elsewhere with a couple of methods in it for validations/operations
I would like to make use of the rails 5 attributes api instead so that I could directly have:
attribute :order_details, Type::OrderDetailType.new
(plus it would make it easier to add validations to each field in my hash)
I've seen examples online for using rails5's attributes api for simple virtual attributes(strings, integers...etc), but have not come across any info on how to implement it for a hash attribute.
Would appreciate it if someone could point me in the right direction.
Extend Type::OrderDetailType from ActiveRecord::Type::Value
You can override cast and serialize method.
def cast(value)
value
end
def serialize(value)
value
end
An example of Money type:
# app/models/product.rb
class Product < ApplicationRecord
attribute :price_in_cents, MoneyType.new
end
class MoneyType < ActiveRecord::Type::Integer
def type_cast(value)
# convert values like '$10.00' to 1000
end
end
product = Product.new(price_in_cents: '$10.00')
product.price_in_cents #=> 1000
Doc: http://edgeapi.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html
Example: http://nithinbekal.com/posts/rails-5-features/
Does somoone knows if it is possible to have array of hstore in rails 4 ? i tryied with
add_column :orders, :frozen_content, :hstore , array: true
but i got
PG::Error: ERROR: malformed array literal:
when i try to save
In principle, yes, but as you've found it isn't being escaped correctly when saved. I've just today logged an issue about that, see https://github.com/rails/rails/issues/11135 (includes a fix patch and some demo code)
This is a bug that exists at least in Rails 4.0.1 .
A pull request was proposed to fix it, but until it is merged you can monkey-patch Rails:
# config/initializers/extensions/postgres.rb
module ActiveRecord
module ConnectionAdapters
class PostgreSQLColumn < Column
module Cast
private
def quote_and_escape(value)
case value
when "NULL"
value
else
"\"#{value.gsub(/(["\\])/, '\\\\\1')}\""
end
end
end
end
end
end
Sidenote, I had trouble testing this in the Rails console because the initializer wasn't getting loaded there. You can do so with:
load "#{Rails.root}/config/initializers/extensions/postgres.rb"
You can sue the activerecord-postgres-hstore gem:
https://github.com/engageis/activerecord-postgres-hstore
From the docs:
Create a hstore-backed field:
class Person < ActiveRecord::Base
serialize :data, ActiveRecord::Coders::Hstore
end
Add fields to to it:
person = Person.new
person.data['foo'] = 'bar'
person.save
Query it:
Perosn.where("data -> 'foo' = 'bar'")
Railscast #345 (which is behind a paywall) covers using hstore in more details, using the activerecord-postgres-hstore gem:
http://railscasts.com/episodes/345-hstore
Note: I haven't tried it with rails 4... YMMV.
We had the following class to process SOAP responses from external API(s) which worked fine in ruby 1.8.7, but it is looking for a table with these columns post migration (which has never been there) to ruby 1.9.2/rails 3.1, How do I handle this migratation?
class SoapResponse < ActiveRecord::Base
def self.columns
#columns ||= [];
end
def self.column(name, sql_type = nil, default = nil, null = true)
columns << ActiveRecord::ConnectionAdapters::Column.new(
name.to_s, default, sql_type.to_s, null)
end
def save(validate = true)
validate ? valid? : true
end
column :soap_payload, :text
serialize :soap_payload
end
You don't (have any migrations for it).
You don't have migrations and you don't inherit from ActiveRecord::Base as that is the database ORM component.
If you use a generator to create the model use --skip-migration to avoid generating the database migration file.
You can still get validations and conversions though, e.g.
class SoapResponse
include ActiveModel::Validations
include ActiveModel::Conversion
If you want some setup data (i.e. Constants, given there is no db! ) you can just define them here (Constants start with uppercase).
I guess you want the serialization capability of ActiveRecord::Base. That seems to be the only thing this class is using it for. If so, try calling this in your class definition:
self.abstract_class = true
Or you could try using ActiveModel::Serialization.
The pattern in your code looks like what's suggested in this answer for table-less AR models in Rails 2.
I have a model that uses a serialized column:
class Form < ActiveRecord::Base
serialize :options, Hash
end
Is there a way to make this serialization use JSON instead of YAML?
In Rails 3.1 you can just
class Form < ActiveRecord::Base
serialize :column, JSON
end
In Rails 3.1 you can use custom coders with serialize.
class ColorCoder
# Called to deserialize data to ruby object.
def load(data)
end
# Called to convert from ruby object to serialized data.
def dump(obj)
end
end
class Fruits < ActiveRecord::Base
serialize :color, ColorCoder.new
end
Hope this helps.
References:
Definition of serialize:
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/base.rb#L556
The default YAML coder that ships with rails:
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/coders/yaml_column.rb
And this is where the call to the load happens:
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/attribute_methods/read.rb#L132
Update
See mid's high rated answer below for a much more appropriate Rails >= 3.1 answer. This is a great answer for Rails < 3.1.
Probably this is what you're looking for.
Form.find(:first).to_json
Update
1) Install 'json' gem:
gem install json
2) Create JsonWrapper class
# lib/json_wrapper.rb
require 'json'
class JsonWrapper
def initialize(attribute)
#attribute = attribute.to_s
end
def before_save(record)
record.send("#{#attribute}=", JsonWrapper.encrypt(record.send("#{#attribute}")))
end
def after_save(record)
record.send("#{#attribute}=", JsonWrapper.decrypt(record.send("#{#attribute}")))
end
def self.encrypt(value)
value.to_json
end
def self.decrypt(value)
JSON.parse(value) rescue value
end
end
3) Add model callbacks:
#app/models/user.rb
class User < ActiveRecord::Base
before_save JsonWrapper.new( :name )
after_save JsonWrapper.new( :name )
def after_find
self.name = JsonWrapper.decrypt self.name
end
end
4) Test it!
User.create :name => {"a"=>"b", "c"=>["d", "e"]}
PS:
It's not quite DRY, but I did my best. If anyone can fix after_find in User model, it'll be great.
My requirements didn't need a lot of code re-use at this stage, so my distilled code is a variation on the above answer:
require "json/ext"
before_save :json_serialize
after_save :json_deserialize
def json_serialize
self.options = self.options.to_json
end
def json_deserialize
self.options = JSON.parse(options)
end
def after_find
json_deserialize
end
Cheers, quite easy in the end!
The serialize :attr, JSON using composed_of method works like this:
composed_of :auth,
:class_name => 'ActiveSupport::JSON',
:mapping => %w(url to_json),
:constructor => Proc.new { |url| ActiveSupport::JSON.decode(url) }
where url is the attribute to be serialized using json
and auth is the new method available on your model that saves its value in json format to the url attribute. (not fully tested yet but seems to be working)
I wrote my own YAML coder, that takes a default. Here is the class:
class JSONColumn
def initialize(default={})
#default = default
end
# this might be the database default and we should plan for empty strings or nils
def load(s)
s.present? ? JSON.load(s) : #default.clone
end
# this should only be nil or an object that serializes to JSON (like a hash or array)
def dump(o)
JSON.dump(o || #default)
end
end
Since load and dump are instance methods it requires an instance to be passed as the second argument to serialize in the model definition. Here's an example of it:
class Person < ActiveRecord::Base
validate :name, :pets, :presence => true
serialize :pets, JSONColumn.new([])
end
I tried creating a new instance, loading an instance, and dumping an instance in IRB, and it all seemed to work properly. I wrote a blog post about it, too.
A simpler solution is to use composed_of as described in this blog post by Michael Rykov. I like this solution because it requires the use of fewer callbacks.
Here is the gist of it:
composed_of :settings, :class_name => 'Settings', :mapping => %w(settings to_json),
:constructor => Settings.method(:from_json),
:converter => Settings.method(:from_json)
after_validation do |u|
u.settings = u.settings if u.settings.dirty? # Force to serialize
end
Aleran, have you used this method with Rails 3? I've somewhat got the same issue and I was heading towards serialized when I ran into this post by Michael Rykov, but commenting on his blog is not possible, or at least on that post. To my understanding he is saying that you do not need to define Settings class, however when I try this it keeps telling me that Setting is not defined. So I was just wondering if you have used it and what more should have been described? Thanks.