Possible to use `attribute` with `store_accessor` - ruby-on-rails

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

Related

How to choose a continent by the country?

I use Ruby on Rails 5.2 and mongoid 7.0
I need to choose a continent by the country
I understand that it should look something like this:
class Place
field :country, type: String
field :continent, type: String
after_save :update_continent
def update_continent
cont = self.country
case cont
when 'United States', 'Grenada'
'NA'
when 'Netherlands', 'Spain'
'EU'
end
self.continent = cont
end
end
Since you indicated you are using Mongoid:
Each Mongoid model class must include Mongoid::Document, per the documentation in https://docs.mongodb.com/mongoid/master/tutorials/mongoid-documents/.
after_save callbacks are normally used for things like creating external jobs, not for setting attributes, because the attribute changes won't be persisted (as the model was already saved). Usually attribute changes are done in before_validation or before_save callbacks. See https://guides.rubyonrails.org/active_record_callbacks.html for the list of available callbacks.
As pointed out by Toby, the case statement is not correctly used. Its result should be assigned like this:
.
def update_continent
self.continent = case self.country
when 'United States', 'Grenada'
'NA'
when 'Netherlands', 'Spain'
'EU'
end
end
You haven't given enough context to be able to answer your question, but since you just want to be pointed in the right direction, and since you seem to be new here I'm happy to give you some pointers.
You're class uses the after_save method as if it is an ActiveRecord Model, but without extending or including anything it's just a Plain Old Ruby Object. To make the after_save callback work you need to at least extend ActiveModel::Callbacks but probably you want to make it a full ActiveRecord Model. To do that in Rails 4 you subclass ActiveRecord::Base and in rails 6 you subclass ApplicationRecord But I don't actually know how it's done in Rails 5.
If you have a normal database in the back end as is usual for rails you don't need to declare the fields, it automatically gets them from the equivalent table in the database (though perhaps this is not true when using Mongoid. I don't know). if you run this command in your terminal in your app base directory: rails generate model Place country:string continent:string it will create the migration file needed to make the database table and the Model file (with whatever the correct superclass is) and you wont need to do all the boilerplate stuff yourself.
You have a variable named cont and you assign a country to it. This will get very confusing given that you also have a separate concept of "continent" Better to not abbreviate your variable names and choose sensible naming.
You're not using the case statement correctly. The output of the statement doesn't automatically get assigned to the the variable you're switching on. You need to read up on Ruby syntax.
Overall I suspect in the long run you would do well to have separate models for Continent and Country. With a Continent having many countries and a country belonging to a continent. Rails is a framework that makes that sort of thing very easy to do and manage. You probably need to read some more and look at examples and videos about the basics of Ruby on Rails.
I highly recommend The Rails Tutorial by Hartl. It's free online. Working through that or an equivalent should give you a much better understanding of how Rails is equipped to handle your situation and how to best utilise it to get the outcome you need. This was indispensable for me when I was first starting out with Rails.

Rails timestamps for specific columns

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

How do I specify column name and types while extending ActiveRecord::Base in Ruby on Rails?

Forgive my ignorance if I am missing something really trivial, I am very new to RoR.
Coming from Django background I remember models being like
class Post(models.Model):
title = models.CharField(max_length=100)
description = models.TextField()
both column name and field type are clearly specified.
Where as, when I write this rails command
> rails g model Post title:string description:text
all I get is
class Post < ActiveRecord::Base
attr_accessible :description, :title
end
Is there a way to write column names and field types while extending ActiveRecord::Base instead of having them only in the migration file?
Thanks, any help is appreciated
attr_accessible is deprecated and strong params replaces it functionality in the controller.
If you want to get a list of the schema information in your model file you can annotate it at the top of the file. This was best practices at some point, however I do not think it is used as much. I personally do not like this and just use the schema.
Post on best practices and annotating:
http://rails-bestpractices.com/posts/68-annotate-your-models
Gem to auto annotate:
https://github.com/ctran/annotate_models
The most wonderful thing about ActiveRecord is that you don't need to do any mapping, as all the fields are being automatically mapped using current schema. Of course default mapping can be overridden if needed, however this is a very rare case.
This approach is called "convention over configuration" and is present all over Rails - it assumes most common parameters for what you are trying to achieve so it saves a lot of unnecessary coding and mapping. It might feel weird at start, especially that you'll need to learn how to override those defaults, but I promise you are gonna love it when you get used to it. :)

Rails 3.2.x associations and attr_accessible

Trying to find the definitive answer on whether active record associations should be in the list of attr_accessible attributes.
I've seen
class Foo
attr_accessible :name
attr_accessible :bars
belongs_to :bar
end
also seen
attr_accessible :bars_id
want to know the proper way to be able to do Foo.new(name: 'name' bar: barvar)
As often the definitive answer is: "It depends™"
Only the attributes you want to mass-assign should be made accessible.
So if you want or need to do…
Foo.new(name: 'name', bar: barvar)
…then you simply have to make bar accessible.
In the end assign_attributes is called which does a simple send("#{attribute_name}=", attribute_value) after checking the accessibility of the attribute.
Some coding style aspects:
Often mass assignment happens when processing the param hash. At least that's where the security problems are lurking. There you rarely have a Bar object but more often a bar_id.
However if you work with model instances, most people prefer using the association methods (as #Andrew Nesbitt wrote) because that often has some advantages (automatic saving, automatic update of the association counterpart, cleaner code, …)
So there are reasons to have one or the other or both.
My personal opinion: One should not waste a lot of time on this topic since Rails 4.0 will have a better solution for parameter sanitizing. (See strong_parameters if you want it in Rails 3, too)
You can avoid needing to make bar_id accessible by using the association builder:
# singular (has_one)
foo = bar.build_foo(name: 'name')
# plural (has_many)
foo = bar.foos.build(name: 'name')
The only time you would need to make an association accessible is if you are using accepts_nested_attributes.
While you can avoid making bars_id (shouldn't that be bar_id?) accessible in your example, the question is if parts of your application still needs access to it. Using active_admin, I had to make the whatever_id accessible to make things work with relations.

Creating readable models in rails

I have just started with Rails and coming from a .net background I find the model inheriting from ActiveRecord is hard to understand, since the don't contain the corresponding attributes for the model. I cannot imagine a new developer exposed to a large code where the models only contains references to other models and business logic.
From my point of view the DataMapper model is much easier to grasp but since ActiveRecord is the defacto standard it feels weird to change the ORM just for this little problem.
DataMapper
class Post
include DataMapper::Resource
property :id, Serial # An auto-increment integer key
property :title, String # A varchar type string, for short strings
property :body, Text # A text block, for longer string data.
property :created_at, DateTime # A DateTime, for any date you might like.
end
ActiveRecord
class Post < ActiveRecord::Base
end
I'm not sure if this is an issue and that people get used to the models without attributes, or how does experienced rails user handle this?
I don't think using the database manager or looking at loads of migrations scripts to find the attributes is an option?
Specifying attr_accessible will make the model more readable but I'm not sure if it's a proper solution for my problem?
Check out the annotate_models plugin on github. It will insert a commented schema for each model in a comment block. It can be installed to run when migrate is.
You don't have to "look at loads of migration scripts to find the attributes" - they're all defined in one place in db/schema.rb.
A few tips:
Load up the Rails console and enter
Post.column_names for a quick
reminder of the attribute names.
Post.columns gives you the column
objects, which shows the datatypes
db/schema.rb contains all the
migration code in one place, so you
can easily see all the column
definitions.
If you are using a
decent editor/IDE there should be a way to
allowing you to jump from the model file
to the migration file. (e.g. Emacs
with ROR or Rinari)

Resources