I'm looking for a way to add a *_updated_at columns for several columns in a record.
It would work similarly to normal Rails timestamps, ie based on convention. If there's a DB column called author_updated_at, the class would automagically update it whenever author attribute changes.
Not too hard to write, but thought I'd ask here in case anyone has done it before or there's a gem around. I'd also be interested to know if there are any performance issues with this approach, though I think it should be negligible if using before_save. There's no extra reads or writes needed.
You might want to try paper_trail - it tracks changes to a model but in a slightly different fashion - instead of adding extra columns for each attribute to track it uses a table to store versions of the model. This gives you a full blown versioning system without doing much work.
You can easily revert to previous versions and track who created a revision etc.
But if all you need is a light-weight solution to store a timestamp for the changes to a limited set of attributes you can use model callbacks to set the *_updated_at columns.
module AttributeTimestamps
extend ActiveSupport::Concern
included do
after_validation(on: :update) do
# Use ActiveModel::Dirty to get the changed attributes
self.previous_changes.each_with_index do |attr|
setter = "#{attr}_updated_at="
self.call(setter, self.updated_at) if self.respond_to? setter
end
end
end
end
class MyModel < ActiveRecord::Base
include AttributeTimestamps
end
I solved with a different version of #max solution.
previous_changes wasn't working for me. I'm using Rails 4.2.
module AttributeTimestampable
extend ActiveSupport::Concern
included do
after_save do
self.changed.each do |attr|
timestamp = "#{attr}_updated_at"
update_column(timestamp, updated_at) if self.respond_to? timestamp
end
end
end
end
Related
In Rails 5, is it possible to use the new attributes API with a field exposed via store_accessor on a jsonb column?
For example, I have:
class Item < ApplicationRecord
# ...
store_accessor :metadata, :publication_date
attribute :publication_date, :datetime
end
Then I'd like to call i = Item.new(publication_date: '2012-10-24'), and have metadata be a hash like: { 'publication_date' => #<DateTimeInstance> }.
However, the attribute call doesn't seem to be doing any coercion.
Hopefully I am missing something--it seems like being able to use these two features in conjunction would be very useful when working with jsonb columns. (Speaking of which, why doesn't the attributes API expose a generic array: true option? That would also be very useful for this case.)
I see that there is a project jsonb_accessor, but it seems a little heavyweight. It also seems to be designed for Rails 4 (I haven't checked whether it supports Rails 5).
You might check out a rather new (as of this writing) gem built atop the Rails 5+ Attributes API: AttrJson. I've recently started using it; some rough edges still, but the author/maintainer seems keen to improve it.
After digging in a little more, I see that the Attributes API (as it currently exists in ActiveRecord) is not really appropriate for handling jsonb data—there would be duplicate info in the attributes hash, etc.
I do think it would be nice if ActiveRecord provided typecasting/coercion for jsonb fields. I see that there is a project jsonb_accessor, but it seems a little heavyweight. It also seems to be designed for Rails 4 (I haven't checked whether it supports Rails 5).
I guess something like this might be in the works for Rails since the ActiveRecord::Type values are actually defined in ActiveModel.
For now I am using the following. I've never really loved Hashie, but this is relatively lightweight and easy to use:
class Item < ApplicationRecord
class Metadata < Hashie::Dash
include Hashie::Extensions::Dash::Coercion
include Hashie::Extensions::Dash::IndifferentAccess
property :publication_date, coerce: Time
def self.dump(obj); obj.as_json; end
def self.load(obj); new(obj); end
end
serialize :metadata, Metadata
store_accessor :metadata, :publication_date
end
There are three questions here, all of them set in bold.
I have a personnel database where historical data is also stored.
Each table has two columns start and stop which indicate the period during which this fact is valid.
When any field of a record changes, a new record is created with start=today, and stop=nil,
and the old record is given a stop=today-1 (or whatever the effective date of the change is instead of today).
When a record is to be deleted, instead of deleting, we simply set stop=today.
This way, it is easy to
give a history of any employee
give a snapshot of what things looked like on a particular date
Firstly this pattern seems common enough that probably I should not need to re-invent the wheel.
Yet in my search all I found were some gems called auditable or papertrail.
From a first glance, they did not seem to do what I want, but perhaps I am mistaken.
Do you know of a gem that does something similar?
If I don't find a solution, what is enough for my purposes is to have a few methods which works for all my models, like
def when(date)
self.where("start <= ? and ? <= stop", ondate, ondate)
end
I thought about inheriting
class MyRecord < ActiveRecord::Base
def when ...
end
end
and having all my models inherit from MyRecord, but this broke everything. Why?
I could just add my methods to ActiveRecord::Base, but that seems wrong.
What is the proper way to add a method to all models?
Answer
Parts 2 and 3 of this question have been answered before here. Since that contains Josh's answer (which I +1ed), as well as why inheriting from MyRecord doesn't work, I did not accept Josh's answer.
I still have not located a gem which maintains a good history, but two out of three ain't bad...
This sounds to me like an excellent opportunity to use an ActiveSupport::Concern. You could write something like:
History.rb
require 'active_support/concern'
module History
extend ActiveSupport::Concern
included do
def self.when(date)
self.where("start <= ? and ? <= stop", ondate, ondate)
end
end
end
Your model
class SomeClass < ActiveRecord::Base
include History
end
You could then call SomeClass.when(some_date_string) on any models that you need to include history on.
A gem I've been using adds methods to models. It recently updated its method names such that one of the method names is now the same as one of my model's pre-existing database attributes.
Are there any workarounds to this problem other than renaming the column in my database and updating all of my code if I wish to stay up-to-date with the gem?
In case it is helpful, to make this more concrete, the gem is PaperTrail, which adds version tracking to models. My model had a pre-existing attribute in the database called version_name, which the latest version of PaperTrail just added as a class_attribute version_name that is used by PaperTrail to define the name of another method.
Not that familiar with PaperTrail (though I've been meaning to look into it). Assuming PaperTrail doesn't have a config option to change the name of *version_name*, you could probably get around it this way in your model:
class Thingy
def version_name_attr
attributes['version_name']
end
def version_name_attr=(val)
attributes['version_name'] = val
end
end
Just use *version_name_attr* whenever you want to access your attribute, and *verson_name* when you want the PaperTrail method.
Something like this is a bit cleaner, but might break things if PaperTrail uses *version_name* internally.
class Thingy
alias_method :paper_trail_version_name, :version_name
def version_name
attributes['version_name']
end
end
In this case, use *paper_trail_version_name* when you want the PaperTrail method. Access to your attribute would remain as you expect it.
I've ran into various problems with various sites over the years with users putting spaces at the start/end of string and text fields. Sometimes these cause formatting/layout problems, sometimes they cause searching problems (ie search order looking wrong even though it isn't really), sometimes they actually crash the app.
I thought it would be useful, rather than putting in a bunch of before_save callbacks as i have done in the past, to add some functionality to ActiveRecord to automatically call .strip on any string/text fields before saving, unless i tell it not to, eg with do_not_strip :field_x, :field_y or something similar at the top of the class definition.
Before i go and figure out how to do this, has anyone seen a nicer solution? Just to be clear, i already know that i can do this:
before_save :strip_text_fields
def strip_text_fields
self.field_x.strip!
self.field_y.strip!
end
but i'm looking for a nicer way.
cheers, max
Here's a handy module that you could drop into lib and include in your models. It doesn't have the exceptions that you mentioned, but it looks for a strip! method which might be good enough. You could add the exceptions feature fairly easily, if needed.
# lib/attribute_stripping.rb
module AttributeStripping
def self.included(context)
context.send :before_validation, :strip_whitespace_from_attributes
end
def strip_whitespace_from_attributes
attributes.each_value { |v| v.strip! if v.respond_to? :strip! }
end
end
Use like this:
class MyModel < ActiveRecord::Base
include AttributeStripping
# ...
end
UPDATE (9/10/2013):
Revisiting this answer a couple of years later, I see how the winds have changed. There's a cleaner way to do it now. Create a module like this:
module AttributeStripper
def self.before_validation(model)
model.attributes.each_value { |v| v.strip! if v.respond_to? :strip! }
true
end
end
and set its method to be invoked at the right time in your model:
class MyModel < ActiveRecord::Base
before_validation AttributeStripper
# ...
end
This module is easier to test since it's not a mixin.
I have dealt with these sort of data integrity issues in various applications.
I used to manipulate the input like that.
But now, the best advice I have actually seen and followed is to store whatever the user types.
Then do post-processing on the backend to do the strip.
Create additional database fields (destripped) if you really want it in the database model table.
The main reason for this is one (primary) thing - when users want to revisit their data, i.e. edit, they're usually gonna expect to see what they typed in. A secondary reason is that you will avoid the possibility that your strip doesn't work right and either mangles the data or actually throw an error.
I've written a plugin for this purpose some time ago. I haven't tried it in a while and it doesn't have tests - so no guaranties that it still works. The upside would be a clean model:
class Story < ActiveRecord::Base
strip_strings :title, :abstract, :text
end
I'm writing a Rails application against a legacy database. One of the tables in this legacy database has a column named object_id. Unfortunately object_id is also an attribute of every object in Ruby, so when ActiveRecord is trying to use these objects to formulate a query it is using the Ruby defined object_id, rather than the value that is in the database.
The legacy application is immense at well over a million lines of code, so simply changing the name of the column in the database would be an option of last resort.
Questions:
1. Is there any way to make ActiveRecord/Rails use an alias or synonym for this column?
2. Is there any way in Ruby to make the object_id method behave differently, depending on who is calling it?
3. Can I simply override the behavior of the object_id method in my model (I assume this is a terrible idea, but had to ask)
Any suggestions are greatly appreciated.
I'm just kind of spitballing here, but you might try something like this:
class Legacy < ActiveRecord::Base
#... all the other stuff
#give yourself a way to access the DB version of object_id
def oid
attributes[:object_id]
end
def oid=(val)
attributes[:object_id]=val
end
#restore ruby's default #object_id implementation
def object_id
super
end
end
Check out alias_attribute http://www.railstips.org/blog/archives/2008/06/20/alias-attribute/ I believe that it does what you are looking for.