Convert an Object to hash then save it to user's column - ruby-on-rails

Could not find nothing close to what I'm trying to do. I want to store an object into a user's column. That column is in the form of an array:
#postgres
def change
add_column :users, :interest, :string, array: true, default: '{}'
end
I have another model called FooBar setup for other use. Each user has unique information inside as I've added a user_id key.
Im trying to make more sense:
def interest
#user = User.find(current_user.id ) # I need the logged in user's id
#support = Support.find(params[:id]) # I need the post's id they are on
u = FooBar.new
u.user_id = #user
u.support_id = #support
u.save # This saves a new Foo object..this is what I want
#user.interest.push(FooBar.find(#user)) # This just stores the object name itself ;)
end
So when I call u1 = FooBar.find(1) I get value return in hash. I want when I say u1.interest I get the same. The reason is, I need to target those keys on the user ie: u1.interest[0].support_id
Is this possible? I've looked over my basic ruby docs and nothing works. Oh..if I passed FooBar.find(#user).inspect I get the hash but not the way I want it.
Im trying to do something similar to stripe. Look at their data key. That's a hash.
Edit for Rich' answer:
I have, literally, a model called UserInterestSent model and table:
class UserInterestSent < ActiveRecord::Base
belongs_to :user
belongs_to :support # you can call this post
end
class CreateUserInterestSents < ActiveRecord::Migration
def change
create_table :user_interest_sents do |t|
t.integer :user_id # user's unique id to associate with post (support)
t.integer :interest_sent, :default => 0 # this will manually set to 1
t.integer :support_id, :default => 0 # id of the post they're on
t.timestamps # I need the time it was sent/requested for each user
end
end
end
I call interest interest_already_sent:
supports_controller.rb:
def interest_already_sent
support = Support.find(params[:id])
u = UserInterestSent.new(
{
'interest_sent' => 1, # they can only send one per support (post)
'user_id' => current_user.id, # here I add the current user
'support_id' => support.id, # and the post id they're on
})
current_user.interest << u # somewhere this inserts twice with different timestamps
end
And the interest not interests, column:
class AddInterestToUsers < ActiveRecord::Migration
def change
add_column :users, :interest, :text
end
end

HStore
I remembered there's a PGSQL datatype called hStore:
This module implements the hstore data type for storing sets of
key/value pairs within a single PostgreSQL value. This can be useful
in various scenarios, such as rows with many attributes that are
rarely examined, or semi-structured data. Keys and values are simply
text strings.
Heroku supports it and I've seen it used on another live application I was observing.
It won't store your object in the same way as Stripe's data attribute (for that, you'll just need to use text and save the object itself), but you can store a series of key:value pairs (JSON).
I've never used it before, but I'd imagine you can send a JSON object to the column, and it will allow you to to use the attributes you need. There's a good tutorial here, and Rails documentation here:
# app/models/profile.rb
class Profile < ActiveRecord::Base
end
Profile.create(settings: { "color" => "blue", "resolution" => "800x600" })
profile = Profile.first
profile.settings # => {"color"=>"blue", "resolution"=>"800x600"}
profile.settings = {"color" => "yellow", "resolution" => "1280x1024"}
profile.save!
--
This means you should be able to just pass JSON objects to your hstore column:
#app/controllers/profiles_controller.rb
class ProfilesController < ApplicationController
def update
#profile = current_user.profile
#profile.update profile_params
end
private
def profile_params
params.require(:profile).permit(:x, :y, :z) #-> z = {"color": "blue", "weight": "heavy"}
end
end
As per your comments, it seems to me that you're trying to store "interest" in a User from another model.
My first interpretation was that you wanted to store a hash of information in your #user.interests column. Maybe you'd have {name: "interest", type: "sport"} or something.
From your comments, it seems like you're wanting to store associated objects/data in this column. If this is the case, the way you're doing it should be to use an ActiveRecord association.
If you don't know what this is, it's essentially a way to connect two or more models together through foreign keys in your DB. The way you set it up will determine what you can store & how...
#app/models/user.rb
class User < ActiveRecord::Base
has_and_belongs_to_many :interests,
class_name: "Support",
join_table: :users_supports,
foreign_key: :user_id,
association_foreign_key: :support_id
end
#app/models/support.rb
class Support < ActiveRecord::Base
has_and_belongs_to_many :users,
class_name: "Support",
join_table: :users_supports,
foreign_key: :support_id,
association_foreign_key: :user_id
end
#join table = users_supports (user_id, support_id)
by using this, you can populate the .interests or .users methods respectively:
#config/routes.rb
resources :supports do
post :interest #-> url.com/supports/:support_id/interest
end
#app/controllers/supports_controller.rb
class SupportsController < ApplicationController
def interest
#support = Support.find params[:support_id] # I need the post's id they are on
current_user.interests << #support
end
end
This will allow you to call #user.interests and bring back a collection of Support objects.
Okay, look.
What I suggested was an alternative to using interest column.
You seem to want to store a series of hashes for an associated model. This is exactly what many-to-many relationships are for.
The reason your data is being populated twice is because you're invoking it twice (u= is creating a record directly on the join model, and then you're inserting more data with <<).
I must add that in both instances, the correct behaviour is occurring; the join model is being populated, allowing you to call the associated objects.
What you're going for is something like this:
def interest_already_sent
support = Support.find params[:id]
current_user.interests << support
end
When using the method I recommended, get rid of the interest column.
You can call .interests through your join table.
When using the code above, it's telling Rails to insert the support object (IE support_id into the current_user (IE user_id) interests association (populated with the UserInterestSelf table).
This will basically then add a new record to this table with the user_id of current_user and the support_id of support.

EDIT
To store Hash into column, I suggest you to use "text" instead
def change
add_column :users, :interest, :text
end
and then set "serialize" to attribute
class User < ActiveRecord::Base
serialize :interest
end
once it's done, you can save hash object properly
def interest
#user = User.find(current_user.id ) # I need the logged in user's id
#support = Support.find(params[:id]) # I need the post's id they are on
u = FooBar.new
u.user_id = #user
u.support_id = #support
u.save # This saves a new Foo object..this is what I want
#user.interest = u.attributes # store hash
#user.save
end

To convert AR object to hash use object.attributes.
To store a custom hash in a model field you can use serialize or ActiveRecord::Store

You can also use to_json method as object.to_json
User.find(current_user.id ).to_json # gives a json string

Related

How to modify an attributes of an ActiveRecord instance before render it

In my Rails project, I have a model class Employee < ActiveRecord::Base, the class has an attribute called compensation, it is a :jsonb data type in Postgresql.
create_table "employee" do |t|
...
t.jsonb "compensation"
...
end
The compensation contains salary, working_hours, start_from, and etc
compensation : {
salary: 50000,
working_hours: 230,
start_from: 2018-12-21,
...
}
What I wanna do is that before an instance of Employee being rendered as JSON and response to the front-end, I need to remove the salary attribute in the compensation. In employee_controller I tried to use
def get_without_salary
employee = Employee.find 2
employee.compensation.delete :salary
jsonapi_render json: employee
end
but the result JSON still contains the salary data.
I can only make it works by:
temp_compensation = employee.compensation.dup
temp_compensation.delete :salary
employee.compensation = temp_compensation
but it is too ugly and confused me why the first way failed.
Can someone explain to me why? Thanks
This seems a rather tricky question. The options for as_json are the following:
includes: => used to include has_many and belongs_to associations
methods: => to include the result of a class method of the model
except: => prevent columns from showig
only: => only show specified columns
I tried some of them to specify only some attributes of the jsonb column but they either dont work or throw a NoMethodError: undefined method 'serializable_hash' for #<Hash:0x00000005082eb0> showing that it tries to serialize your jsonb but fails.
I think a clean way of doing it may be to have a method filtered_compensation in your model where you define the json you want to send and prevent compensation from showing.
def get_without_salary
employee = Employee.find 2
jsonapi_render json: employee.as_json(methods: [:filtered_compensation], except: [:compensation])
end
Where filtered_compensation is declared in your model
def filtered_compensation
compensation.except 'salary'
end
I think it complies with being clean, separating concerns and makes your method filtered_compensation to be used elsewhere.
Note that instead of having in your response the attribute compensation, you would find filtered_compensation as that is the name of the method.
After a few days thinking, I came up the solution that first converts the active_record instance into a hash, then delete the salary in the hash, and then converts the hash into an active_record instance. This is a better solution comparing to mutating an object on the fly.
def get_without_salary
employee = Employee.find 2
filtered_employee = filter_salary employee
jsonapi_render json: filtered_employee
end
def filter_salary(employee)
record_hash = employee.as_json.deep_symbolize_keys!
record_hash[:compensation]&.delete :salary
Employee.new(record_hash)
end

Access to model instance up the where chain in Rails

Is it possible to, within the record found through an association, retain access to the related model instance which found it?
Example:
class Person < ApplicationRecord
has_many :assignments
attr_accessor :info_of_the_moment
end
p = Person.first
p.info_of_the_moment = "I don't want this in the db"
assignment = p.assignments.first
assignment.somehow_get_p.info_of_the_moment # or some such magic!
And/or is there a way to "hang on to" the parameters of a scope and have access to them from within the found model instance? Like:
class Person < ApplicationRecord
has_many :assignments
attr_accessor :info_of_the_moment
scope :fun_assignments, -> (info) { where(fun: true) }
end
class Assignment < ApplicationRecord
belongs_to :person
def get_original_info
# When I was found, info was passed into the scope. What was it?
end
end
You can add your own extension methods to an association and those methods can get at the association's owner through proxy_association:
has_many :things do
def m
# Look at proxy_association.owner in here
end
end
So you could say things like:
class Person < ApplicationRecord
has_many :assignments do
def with_info
info = proxy_association.owner.info_of_the_moment
# Then we wave our hands and some magic happens to encode
# `info` into a properly escaped SQL literal that we can
# toss in a `select` call. If you're working with PostgreSQL
# then JSON would be a reasonable first choice if the info
# was, say, a hash.
#
# The `::jsonb` in the `select` call is there to tell everyone
# that the `info_of_the_moment` column is JSON and should be
# decoded as such by ActiveRecord.
encoded_info = ApplicationRecord.connection.quote(info.to_json)
select("assignments.*, #{encoded_info}::jsonb as info_of_the_moment")
end
end
#...
end
p = Person.first
p.info_of_the_moment = { 'some hash' => 'that does', 'not go in' => 'the database' }
assignment = p.assignments.with_info.first
assignment.info_of_the_moment # And out comes the hash but with stringified keys regardless of the original format.
# These will also include the `info_of_the_moment`
p.assignments.where(...).with_info
p.assignments.with_info.where(...)
Things of note:
All the columns in select show up as methods even when they're not part of the table in question.
You can add "extension" methods to an association by including a block with those methods when calling the association's method.
An SQL SELECT can include values that aren't columns, literals work just fine.
What format you use to tunnel your extra information through the association depends on the underlying database.
If the encoded extra information is large then this can get expensive.
This is admittedly a bit kludgey and brittle so I'd agree with you that rethinking your whole approach is a better idea.

How to pull information from associated model (Rails)

I'm running a search using the Ransack gem.
Controller code (for ItemsController)
#q = Item.search(params[:q])
#search_results = #q.result
After running the search, #search_results contains multiple Items (Item 1, Item 2, Item 3, …). Where each Item is a hash:
Item = { "id" => "12", "name" => "shovel", "user_id" => "2", "type" => "A" }
In this case, for each Item hash, I want to add an additional key-value pair that translates the user_id into the name that is associated with that instance of the User model (i.e., User has_many :Items and Item belongs_to :Users)
I tried in the same controller code section
#search_results.each do |item|
item[:user_name] = User.find_by_id(item.user_id).name
end
But I get an error that I can't write to item. I suspect Ransack has something to do with it though, because if I just print #search_results, instead of getting the actual Item data, I get #<ActiveRecord::Relation::ActiveRecord_Relation_Item:0x000001025e6c78>
Help greatly appreciated!
Btw, in case there's even an easier way, the reason I want to add another key/value pair with the user_name is because the ultimate output is a JSON that is being picked up by another part of the codebase, and we don't want to run two separate database queries.
Firstly, you're receiving a ruby object back, which I take means you can create a set of attributes for in the model. Why don't you try using a getter in your item model to set for you:
#app/models/item.rb
Class Item < ActiveRecord::Base
belongs_to :user #-> needs to be singular
attr_accessor :user_name
def user_name
self.user.name
end
end
This means that instead of having to append a user_name attribute from your controller, every Item object will automatically have the user_name attribute anyway
A refactor of this would also be to use the delegate method inside your Item model:
Class Item < ActiveRecord::Base
belongs_to :user
delegate :name, to: :user, prefix: true #-> allows you to call #item.user_name
end

Creating a Rails change log

I am pretty new to rails (and development) and have a requirement to create a change log. Let's say you have an employees table. On that table you have an employee reference number, a first name, and a last name. When either the first name or last name changes, I need to log it to a table somewhere for later reporting. I only need to log the change, so if employee ref 1 changes from Bill to Bob, then I need to put the reference number and first name into a table. The change table can have all the columns that mnight change, but most only be populated with the reference number and the changed field. I don't need the previous value either, just the new one. hope that makes sense.
Looked at gems such as paper trail, but they seem very complicated for what I need. I don't ever need to manipulate the model or move versions etc, I just need to track which fields have changed, when, and by whom.
I'd appreciate your recommendations.
If you insist on building your own changelog, based on your requirements you can do so using a few callbacks. First create your log table:
def up
create_table :employee_change_logs do |t|
t.references :employee
# as per your spec - copy all column definitions from your employees table
end
end
In your Employee model:
class Employee < ActiveRecord::Base
has_many :employee_change_logs
before_update :capture_changed_columns
after_update :log_changed_columns
# capture the changes before the update occurs
def capture_changed_columns
#changed_columns = changed
end
def log_changed_columns
return if #changed_columns.empty?
log_entry = employee_change_logs.build
#changed_columns.each{|c| log_entry.send(:"#{c}=", self.send(c))}
log_entry.save!
end
end
I recommend the gem vestal_versions.
To version an ActiveRecord model, simply add versioned to your class like so:
class User < ActiveRecord::Base
versioned
validates_presence_of :first_name, :last_name
def name
"#{first_name} #{last_name}"
end
end
And use like this:
#user.update_attributes(:last_name => "Jobs", :updated_by => "Tyler")
#user.version # => 2
#user.versions.last.user # => "Tyler"
The first thing we did was put an around filter in the application controller. This was how I get the current_employee into the employee model, which was the challenge, especially for a newbie like me!
around_filter :set_employee_for_log, :if => Proc.new { #current_account &&
#current_account.log_employee_changes? && #current_employee }
def set_employee_for_log
Thread.current[:current_employee] = #current_employee.id
begin
yield
ensure
Thread.current[:current_employee ] = nil
end
end
end
Next, in the employee model I defined which fields I was interested in monitoring
CHECK_FIELDS = ['first_name', 'last_name', 'middle_name']
then I added some hooks to actually capture the changes IF logging is enabled at the account level
before_update :capture_changed_columns
after_update :log_changed_columns, :if => Proc.new { self.account.log_employee_changes? }
def capture_changed_columns
#changed_columns = changed
#changes = changes
end
def log_changed_columns
e = EmployeeChangeLog.new
Employee::CHECK_FIELDS.each do |field|
if self.send("#{field}_changed?")
e.send("#{field}=", self.send(field))
end
end
if e.changed?
e.update_attribute(:account_id, self.account.id)
e.update_attribute(:employee_id, self.id)
e.update_attribute(:employee_ref, self.employee_ref)
e.update_attribute(:user_id, Thread.current[:current_employee])
e.save
else return
end
end
And that;s it. If the account enables it, the app keeps an eye on specific fields and then all changes to those fields are logged to a table, creating an simple audit trail.

rails dynamic attributes

I'd like to have a number of dynamic attributes for a User model, e.g., phone, address, zipcode, etc., but I would not like to add each to the database. Therefore I created a separate table called UserDetails for key-value pairs and a belongs_to :User.
Is there a way to somehow do something dynamic like this user.phone = "888 888 8888" which would essentially call a function that does:
UserDetail.create(:user => user, :key => "phone", :val => "888 888 8888")
and then have a matching getter:
def phone
UserDetail.find_by_user_id_and_key(user,key).val
end
All of this but for a number of attributes provided like phone, zip, address, etc., without arbitrarily adding a ton of of getters and setters?
You want to use the delegate command:
class User < ActiveRecord:Base
has_one :user_detail
delegate :phone, :other, :to => :user_detail
end
Then you can freely do user.phone = '888 888 888' or consult it like user.phone. Rails will automatically generate all the getters, setters and dynamic methods for you
You could use some meta-programming to set the properties on the model, something like the following: (this code was not tested)
class User < ActiveRecord:Base
define_property "phone"
define_property "other"
#etc, you get the idea
def self.define_property(name)
define_method(name.to_sym) do
UserDetail.find_by_user_id_and_key(id,name).val
end
define_method("#{name}=".to_sym) do |value|
existing_property = UserDetail.find_by_user_id_and_key(id,name)
if(existing_property)
existing_property.val = value
existing_property.save
else
new_prop = UserDetail.new
new_prop.user_id = id
new_prop.key = name
new_prop.val = value
new_prop.save
end
end
end

Resources