Overwriting all default accessors in a rails model - mysql

class Song < ActiveRecord::Base
# Uses an integer of seconds to hold the length of the song
def length=(minutes)
write_attribute(:length, minutes.to_i * 60)
end
def length
read_attribute(:length) / 60
end
end
This is an easy example by rails api doc.
Is it possible overwrite all attributes for a model without overwrite each one?

Do you look for something like that? Don't know why you would want to do it, but here you go :)
class Song < ActiveRecord::Base
self.columns_hash.keys.each do |name|
define_method :"#{name}=" do
# set
end
define_method :"#{name}" do
# get
end
# OR
class_eval(<<-METHOD, __FILE__, __LINE__ + 1)
def #{name}=
# set
end
def #{name}
# get
end
METHOD
end
end

I'm not sure of a use case where this would be a good idea. However, all rails models dynamically have their properties assigned to them (assuming it isn't already in the class). The answer is partially in your question.
You can override the read_attribute() and write_attribute() methods. That would apply your transformations to every attribute whether they were written to by the accessor or populated in bulk in the controller. Just be careful to not mutate important attributes like the 'id' attribute.
Ruby has a shortcut that is used in rails code a fair bit that can help you. It's the %w keyword. %w will create an array of words based on the symbols inside the parentheses. Because it's an array you can do useful things like this:
#excludes = %w(id, name)
def read_attribute name
value = super
if(not #excludes.member? name)
value = value.to_i * 60
end
value
end
def write_attribute name, value
if(not #excludes.member? name)
value = value.to_i / 60
end
super
end
That should get you started. There are more advanced constructs like using lambdas, etc. Keep in mind you should write some thorough unit tests to make sure you don't have any unintended consequences. You may have to include more attribute names in the list of excludes.
edit: (read|write)_attributes -> (read|write)_attribute

Related

Performance difference in Concern method ran with Hook vs on Model?

Basically I notice a big performance difference in dynamically overriding a getter for ActiveRecord::Base models within an after_initialize hook and simply within the model itself.
Say I have the following Concern:
module Greeter
extend ActiveSupport::Concern
included do
after_initialize { override_get_greeting }
end
def override_get_greeting
self.class::COLS.each do |attr|
self.class.class_eval do
define_method attr do
"Hello #{self[attr]}!"
end
end
end
end
end
I then have the following model, consisting of a table with names.
CREATE TABLE 'names' ( 'name' varchar(10) );
INSERT INTO names (name) VALUES ("John")
class Name < ActiveRecord::Base
COLS = %w("name")
include Greeter
end
john = Name.where(name: 'John').first
john.name # Hello John!
This works fine. However, if I try to do this a more Rails way it is significantly slower.
Essentially, I want to simply pass a parameter into Greeter method that contains COLS and then overrides the getters. It'll look something like:
# Greeter
module Greeter
extend ActiveSupport::Concern
def override_get_greeting(cols)
cols.each do |attr|
self.class.class_eval do
define_method attr do
"Hello #{self[attr]}!"
end
end
end
end
end
# Name
class Name < ActiveRecord::Base
include Greeter
override_get_greeting [:name]
end
Now Name.where(name: 'John').first.name # Hello John! is about 2 seconds slower on the first call.
I can't put my finger in it. I have an assumption that the the application is just slower to start with the first example, but not really sure.
I prefer the second example but the performance difference is a big no no.
Has anyone came across something like this?
Unless the real application code is radically different to what you've shown above, there's no way this should be causing a 2 second performance hit!
However, it's still a needlessly verbose and inefficient way to write the code: You're redefining methods on on the class instance, every time you initialize the class.
Instead of using after_initialize, you can just define the methods once. For example, you could put this in the Greeter module:
included do |klass|
klass::COLS.each do |attr|
define_method attr do
"Hello #{self[attr]}!"
end
end
end
Also worth noting is that instead of self[attr], you may instead wish to use super(). The behaviour will be the same (assuming no other overrides are present), except that an error will be raised if the column does not exist.

ActiveModelSerializers Polymorphic Json

Been wrestling with trying to get polymorphic serializers working and testing data via rspec. Just upgraded to 0.10+
I found this post, which makes a lot of sense, and does give me a entry into generating the serializations, however, when doing it for polymorphs, I never get the type and id properly named (expecting to see asset_id and asset_type nested)
{:id=>1,
:label=>"Today I feel amazing!",
:position=>0,
:status=>"active",
:media_container_id=>1,
:asset=>
{:id=>4
Test ActiveModel::Serializer classes with Rspec
class MediaSerializer < ApplicationSerializer
attributes :id,
:label,
has_one :asset, polymorphic: true
end
I noticed that the tests dont even seem to properly add the polymorphic identifiers either (ie asset_id, asset_type -- or in the test case imageable_id, imageable_type)
https://github.com/rails-api/active_model_serializers/commit/045fa9bc072a04f5a94d23f3d955e49bdaba74a1#diff-c3565d7d6d40da1b2bf75e13eb8e6afbR36
If I go straight up MediaSerialzer.new(media) I can poke at the .associations, but I cant seem to get them to render as if I was generating a full payload
From the docs
https://github.com/rails-api/active_model_serializers
serializer_options = {}
serializer = SomeSerializer.new(resource, serializer_options)
serializer.attributes
serializer.associations
Im pretty sure Im missing something/doing something wrong - any guidance would be great.
Thanks
It isn't easy to get the effect you are looking for, but it is possible.
You can access the hash generated by the serializer by overriding the associations method.
class MediaSerializer < ApplicationSerializer
attributes :id,
:label,
has_one :asset, polymorphic: true
def associations details
data = super
data[:asset] = relabel_asset(data[:asset])
data
end
def relabel_asset asset
labelled_asset = {}
asset.keys.each do |k|
labelled_asset["asset_#{k}"] = asset[k];
end
labelled_asset
end
end
I learnt alot about ActiveModelSerializer to get the hang of this! I referred to Ryan Bates' podcast on the topic:
http://railscasts.com/episodes/409-active-model-serializers
In there he describes how you can override the attributes method and call super to get access to the hash generated by the serializer. I guessed I could do the same trick for the associations method mentioned in your post. From there it takes a little bit of Ruby to replace all the keys, but, if I have understood correctly what you require, it is technically possible.
Hope that helps!

ActiveRecord, Intentionally truncate string to db column width

In Rails 4, ActiveRecord and it's MySQL adapter are set up so if you try to save an attribute in an AR model to a MySQL db, where the attribute string length is too wide for the MySQL column limits -- you'll get an exception raised.
Great! This is much better default than Rails3, where it silently truncated the string.
However, occasionally I have an attribute that I explicitly want to be simply truncated to the maximum size allowed by the db, with no exception. I'm having trouble figuring out the best/supported way to do this with AR.
It should ideally happen as soon as the attribute is set, but I'd take it happening on save. (This isn't exactly a 'validation', as I never want to raise, just truncate, but maybe the validation system is the best supported way to do this?)
Ideally, it would automatically figure out the db column width through AR's db introspection, so if the db column width changed (in a later migration), the truncation limit would change accordingly. But if that's not possible, I'll take a hard-coded truncation limit.
Ideally it would be generic AR code that would work with any db, but if there's no good way to do that I'd take code that only worked for MySQL
You could truncate your data before inserting in db with a before_save or a before_validation
See Active Record Callbacks — Ruby on Rails Guides and ActiveRecord::Callbacks
You can retrieve informations on your table with MODEL.columns and MODEL.columns_hash.
See ActiveRecord::ModelSchema::ClassMethods
For example (not tested):
class User < ActiveRecord::Base
before_save :truncate_col
......
def truncate_col
col_size = User.columns_hash['your_column'].limit
self.your_column = self.your_column.truncate(col_size)
end
end
I'm pretty sure you can accomplish this with a combination of ActiveRecord callbacks and ConnectionsAdapters. ActiveRecord contains several callbacks you can override to perform specific logic at different points during the save flow. Since the exception is being thrown at save, I would recommend adding your logic to the before_save method. Using the column ConnectionAdapter you should be able to determine the limit of the column you wish to insert, though the logic will most likely be different for strings vs ints, etc. Off the top of my head you'll probably want to implement something like:
class User < ActiveRecord::Base
def before_save
limit = User.columns_hash['attribute'].limit
self.attribute = self.attribute[0..limit-1] if self.attribute.length > limit
end
end
The above example is for a string, but this solution should work for all connection adapters assuming they support the limit attribute. Hopefully that helps.
I'd like to address a few points:
If the data type of your_column is text, in Rails 4 User.columns_hash['your_column'].limit will return nil. It returns a number in case of int or varchar.
The text data type in MySQL has a storage limit of 64k. Meaning truncating upon char length is not enough if the content has non ascii chars like ç which needs more than 1 byte to be stored.
I've bumped into this problem very recently, here is a hotfix for it:
before_save :truncate_your_column_to_fit_into_max_storage_size
def truncate_your_column_to_fit_into_max_storage_size
return if your_column.blank?
max_field_size_in_bytes = 65_535
self.your_column = your_column[0, max_field_size_in_bytes]
while your_column.bytesize > max_field_size_in_bytes
self.your_column = your_column[0..-2]
end
end
Here's my own self-answer, which truncates on attribute set (way before save). Curious if anyone has any feedback. It seems to work!
# An ActiveRecord extension that will let you automatically truncate
# certain attributes to the maximum length allowed by the DB.
#
# require 'truncate_to_db_limit'
# class Something < ActiveRecord::Base
# extend TruncateToDbLimit
# truncate_to_db_limit :short_attr, :short_attr2
# #...
#
# Truncation is done whenever the attribute is set, NOT waiting
# until db save.
#
# For a varchar(4), if you do:
# model.short_attr = "123456789"
# model.short_attr # => '1234'
#
#
# We define an override to the `attribute_name=` method, which ActiveRecord, I think,
# promises to call just about all the time when setting the attribute. We call super
# after truncating the value.
module TruncateToDbLimit
def truncate_to_db_limit(*attribute_names)
attribute_names.each do |attribute_name|
ar_attr = columns_hash[attribute_name.to_s]
unless ar_attr
raise ArgumentError.new("truncate_to_db_limit #{attribute_name}: No such attribute")
end
limit = ar_attr.limit
unless limit && limit.to_i != 0
raise ArgumentError.new("truncate_to_db_limit #{attribute_name}: Limit not known")
end
define_method "#{attribute_name}=" do |val|
normalized = val.slice(0, limit)
super(normalized)
end
end
end
end

Best way to declare a rails array field?

What is the best way/practice to declare a Rails(4) array field (with mysql database)? I need to store some ids into that array. I tried to do this using the ActiveRecord Serializer and I customized the attribute accessors so my field can behave like an array.
class OfficeIds < ActiveRecord::Base
serialize :office_ids
def office_ids=(ids)
ids = ids.join(",") if ids.is_a?(Array)
write_attribute(:office_ids, ids)
end
def office_ids
(read_attribute(:office_ids) || "").split(",")
end
end
I feel that this is not the best approach for this kind of situation. Any help would be appreciated. Thanks!
If you're using the serializer, there's no need to write a wrapper method for this. You should be able to assign arbitrary objects to that field:
ids = OfficeIds.new
ids.office_ids = [ 1, 2, 3 ]
ids.save
It is rather odd to have a model called OfficeIds though, as a plural name for this willc cause all kinds of trouble. Are you sure you don't want a traditional has_many relationship for these?

How to validate a json type attribute

Rails 4.0.4 w/ Postgres
Ruby 2.1.1
I am using a model with a JSON type attribute.
here the migration
def change
create_table :my_models do |t|
t.json :my_json_attribute, :null => false
t.timestamps
end
end
I want to validate this attribute in my form before saved or updated in the database.
Today I am getting a nasty JSON parser error (JSON::ParserError) instead of a friendly message on my form .. I am purposely giving an incorrect JSON input in my form to check if validation works and if I get a friendly message asking to verify the JSON string ... but I am not even sure how to check whether attribute_in_json_format was called
In my model class, I have something like this:
class MyModel < ActiveRecord::Base
validate_presence_of :my_json_attribute
validate :attribute_in_json_format
protected
def attribute_in_json_format
errors[:base] << "not in JSON format" unless my_json_attribute.is_json?
end
end
Created an initializer string.rb:
require 'json'
class String
def is_json?
begin
!!JSON.parse(self)
rescue
false
end
end
end
I am not getting any success ... I am still going to the MultiJson::ParseError instead of going through the validate.
Any suggestion?
I took my inspiration from this stackoverflow thread
I am a bit late, but I think that your approach is in the right direction. However, you don't need to monkey-patch the String class. You can do this:
validate :attribute_in_json_format
private
def attribute_in_json_format
JSON.parse(my_json_attribute.to_s)
rescue JSON::ParserError
errors[:base] << "not in JSON format"
end
Maybe using store_accessor would help.
class MyModel < ActiveRecord::Base
store_accessor :my_jason_attribute
validate_presence_of :my_json_attribute
...
Here is the documentation. Check the NOTE:
NOTE - If you are using PostgreSQL specific columns like hstore or
json there is no need for the serialization provided by store. Simply
use store_accessor instead to generate the accessor methods. Be aware
that these columns use a string keyed hash and do not allow access
using a symbol.
Hope this helps.