After updating object attributes in my db, I can't seem to retrieve the values back in a subsequent method call. At the point I attempt to retrieve the data, I've confirmed the attributes have updated in my db.
def task
update_objects(data)
retrieve_data
end
def update_objects(data)
data.each do |item|
keyword = self.keywords.find_by_description(item.keyword)
keyword.update_attributes(:total_value => item.totalValue.to_f, :avg_revenue_per_transaction => item.revenuePerTransaction.to_f)
end
end
def retrieve_data
keywords = self.keywords # The updated attributes in keywords are nil
# Do stuff with keywords
end
This is because the keywords collection has probably already loaded before you call retrieve_data. However, calling find_by_description doesn't make use of the loaded collection (since it's using the query API) and fetches a new object from the database directly which you are updating. The original loaded collection doesn't know about this. There are several ways to fix this:
You can call reload to refresh the collection from the database:
def retrieve_data
keywords = self.keywords(true)
end
Or don't use the query API, but rather use the collection itself:
keyword = self.keywords.detect{|k| k.description == item.keyword}
Or simply avoid loading the collection in advance.
Related
client_skipped_day_controller.rb
class ClientSkippedDaysController < ApplicationController
before_action :check_client_on_exist, only: [:create]
def index
#client_skipped_days = ClientSkippedDay.order_by(params[:sort_by], params[:direction])
if params[:date].present?
#client_skipped_days = #client_skipped_days.where('skipped_at = ?', Date.parse(params[:date]))
end
render json: #client_skipped_days, status: :ok
end
def create
#client_skipped_days = ClientSkippedDay.create!(client_skipped_days_params)
render json: #client_skipped_days, status: :created
end
def destroy
end
private
def client_skipped_days_params
params.permit(client_skipped_days: %i[client_id skipped_at])[:client_skipped_days]
end
def check_client_on_exist
client_skipped_days_params.each do |day|
ClientSkippedDay.find_by(day)&.destroy
end
end
end
My code works if I try to delete only one record, like a :
Parameters: {"client_skipped_days"=>[{"client_id"=>533, "skipped_at"=>"2019-02-24"}], "client_skipped_day"=>{}}
But if I try to delete each hash in the array, it's didn't work :(
Parameters: {"client_skipped_days"=>[{"client_id"=>533, "skipped_at"=>"2019-02-24"}, {"client_id"=>512, "skipped_at"=>"2019-02-24"}], "client_skipped_day"=>{}}
Only one record will be deleted, but how to add the ability to delete all records? which coincide with the parameters that come from the controller?
And it must be a flexible system to remove if 1 hash in the array and immediately a collection of hashes in the array. Tell me how to do it.
Instead of looping over the params and finding each record one by one you could also consider using multiple #where queries combining them together with the use of #or and loop over the resulting records.
def client_skipped_days_params
params.permit(client_skipped_days: [:client_id, :skipped_at])
# removed `.values` ^
end
def check_client_on_exist
destroyed_records, undestroyed_records =
client_skipped_days_params
.fetch(:client_skipped_days, []) # get the array or use an empty array as default
.map(&ClientSkippedDay.method(:where)) # build individual queries
.reduce(ClientSkippedDay.none, :or) # stitch the queries together using #or
.partition(&:destroy) # call #destroy on each item in the collection, separating destroyed once from undestroyed once
end
In the above example the resulting destroyed records are present in the destroyed_records variable and the records that could not be destroyed are present in the undestroyed_records variable. If you don't care about the result you can leave this out. If you want to raise an exception if a record cannot be destroyed use #destroy! instead (call upon each collection item).
Alternatively you can destroy all records by calling #destroy_all (called upon the collection), but it will simply return an array of records without differentiating the destroyed records from the undestroyed records. This method will still instantiate the records and destroy them one by one with the advantage that callbacks are still triggered.
The faster option is calling #delete_all (called upon the collection). This will destroy all records with one single query. However records are not instantiated when destroyed, meaning that callbacks will not be triggered.
def check_client_on_exist
destroyed_record_count =
# ...
.reduce(ClientSkippedDay.none, :or)
.delete_all # delete all records with a single query (without instantiation)
end
references:
ActionController::Parameters#fetch
Array#map
ActiveRecord::QueryMethods#none
Enumerable#reduce
Enumerable#partition
You need to loop over your array instead of just taking the first value out of it. I don’t understand the params that you have, so I’m assuming that you want to do your find_by using the Hash of client_id and skipped_at.
Also, Ruby 2.3.0 introduced the safe navigation operator, which is what that &. is if you aren’t used to it. http://mitrev.net/ruby/2015/11/13/the-operator-in-ruby/
Since find_by either returns an ActiveRecord object or nil, it’s a great time to use the safe navigation operator to shorten things up.
def client_skipped_days_params
params.permit(client_skipped_days: %i[client_id skipped_at])[:client_skipped_days]
end
def check_client_on_exist
client_skipped_days_params.each do |day|
ClientSkippedDay.find_by(day)&.destroy
end
end
Note, I’m not sure what your client_skipped_day Hash is. I assumed you’re making it possible to delete a single day, or delete in bulk. I would warn against having it do two things. Just make the client always send an array for this action and things will be easier for you. If you can do that, then you can make client_skipped_days required.
def client_skipped_days_params
params.require(:client_skipped_days).permit(%i[client_id skipped_at])
end
This will raise a 422 error to the client if they don’t provide the client_skipped_days key.
If this isn’t possible, then you’ll need to add an if to check_on_exist to make sure that client_skipped_days_params is not null (because they’re using client_skipped_day).
I have two data sources: my database and a third party API. The third-party API is the "source of truth" but I want a user to be able to "bookmark" an item from the third-party API which will then persist it in my database.
The challenge I'm facing is displaying both sets of items in the same list without too much complexity. Here's an example:
Item 1 (not bookmarked, from third-party API)
Item 2 (bookmarked, persisted locally)
Item 3 (bookmarked, persisted locally)
Item 4 (not bookmarked, from third-party API)
...etc
I want the view to fetch the list of all items from the controller and have 0 idea of where the items came from, but should only know whether or not each item is bookmarked so that it can be displayed (e.g. so the user can mark an unbookmarked item as bookmarked).
Generics would be one way to solve this in other languages, but alas, Ruby doesn't have generics (not complaining). In Ruby/Rails, what's the best way to wrap/structure these models so the view only has to worry about one type of item (when in reality there are two types behind the scenes?)
I'd suggest coming up with an object that takes care of fetching the items from both the third-party API and your database, the result of such operation would be an array of items that respond to the same methods, no matter where they came from.
Here's an example on how I'd go about it:
class ItemsController < ApplicationController
def index
#items = ItemRepository.all
end
end
In the code above ItemRepository is responsible for fetching items from both the database and the third party API, the view would then iterate over the #items instance variable.
Here's a sample implementation of the ItemRepository:
class ItemRepository
def self.all
new.all
end
# This method merges items from the API and
# the database into a single array
def all
results_from_api + local_results
end
private
def results_from_api
api_items.map do |api_item|
ResultItem.new(name: api_item['name'], bookmarked: false)
end
end
# This method fetches the items from the API and
# returns an array
def api_items
# [Insert logic to fetch items from API here]
end
def local_results
local_items.map do |local_item|
ResultItem.new(name: local_item.name, bookmarked: true)
end
end
# This method is in charge of fetching items from the
# database, it probably would use the Item model for this
def local_items
Item.all
end
end
The final piece of the puzzle is the ResultItem, remember ItemRepository.all will be returning an array containing objects of this type, so all you need to do is store the information that the view needs from each item on this class.
In this example I assume that all the view needs to know about each item is its name and whether it has been bookmarked or not, so ResultItem responds to the bookmarked? and name methods:
class ResultItem
attr_reader :name
def initialize(name:, bookmarked:)
#name = name
#bookmarked = bookmarked
end
def bookmarked?
!!#bookmarked
end
end
I hope this helps :)
PS. Sorry if some of the class names are too generic I couldn't come up with anything better
Short description
I need to save a field to a table. I used to do this from the controller and it worked perfectly, but now I need to set this field from the service instead. I am using attr_accessor but am not able to get it to work properly.
Long description
I wrote a service (ToolService) that uses an api to create an array of hashes. I have previously saved this array to the object via the controller.
Controller:
1: class ToolsController < ApplicationController
2: def create
3: tool_hash = params.delete('tool')
4: #tool = Tool.new
5: # blah blah get params
6: t = ToolService.new(# pass params to initialize service)
7: #tool.all_data = t.run_tool_report(# pass params to get result)
8: end
9: end
Service:
class ToolService
attr_accessor :all_data
def initialize(# params)
# initializing stuff
end
def run_tool_report(# params, including array_of_tools)
#all_data = Array.new # create an array to hold all hashes of data
array_of_tools.each do |each tool|
# run all api queries
#each_tool_data = # hash of query results
#all_data << #each_tool_data # add each hash of results to array
end
return #all_data
end
end
This works as expected. However, I need to implement Delayed Jobs because this query takes a long time. So, in the controller I have changed line 7 to t.delay.run_tool_report(# pass params to get result). I thought that including attr_accessor :all_data in the service would allow the service write to the #tool.all_data field in the table, but this doesn't seem to be the case.
When I use #tool.delay.all_data = t.run_tool_report(# pass params to get result), #tool.all_data is set to the id of the delayed job, not the array of results.
So, am I using attr_accessor incorrectly? Or is there some other way to set this field in the table?
Delayed job comes in handy when you want to run task asynchronously. When you write t.delay.run_tool_report it creates an entry in the delayed_jobs model to be run in the background. This object is returned to you in the #tool.all_data. If you want the result of the run_tool_report, you need to run without delay and optimise your queries. Preloading/eagerloading and caching techniques might come handy.
I have a class I've extended from ActiveRecord::Base...
class Profile < ActiveRecord::Base
and I collect the records from it like so...
records = #profile.all
which works fine, but it doesn't seem that I can successfully Update the attributes. I don't want to save them back to the database, just modify them before I export them as JSON. My question is, why can't I update these? I'm doing the following (converting date formats before exporting):
records.collect! { |record|
unless record.term_start_date.nil?
record.term_start_date = Date.parse(record.term_start_date.to_s).strftime('%Y,%m,%d')
end
unless record.term_end_date.nil?
record.term_end_date = Date.parse(record.term_end_date.to_s).strftime('%Y,%m,%d')
end
record
}
At first I had just been doing this in a do each loop, but tried collect! to see if it would fix things, but no difference. What am I missing?
P.S. - I tried this in irb on one record and got the same results.
I suggest a different way to solve the problem, that keeps the logic encapsulated in the class itself.
Override the as_json instance method in your Profile class.
def as_json(options={})
attrs = super(options)
unless attrs['term_start_date'].nil?
attrs['term_start_date'] = Date.parse(attrs['term_start_date'].to_s).strftime('%Y,%m,%d')
end
unless attrs['term_end_date'].nil?
attrs['term_end_date'] = Date.parse(attrs['term_end_date'].to_s).strftime('%Y,%m,%d')
end
attrs
end
Now when you render the records to json, they'll automatically use this logic to generate the intermediate hash. You also don't run the risk of accidentally saving the formatted dates to the database.
You can also set up your own custom option name in the case that you don't want the formatting logic.
This blog post explains in more detail.
Try to add record.save! before record.
Actually, by using collect!, you just modifying records array, but to save modified record to database you should use save or save! (which raises exception if saving failed) on every record.
Does anyone know if its possible to create a model instance and apply the ID and any other attributes without having to load it from the database? I tried doing this, but the associations are not fetched from the database :( Any ideas?
EDIT
What I want to accomplish is simply this:
Fetch an existing record from the database.
Store as "hashed" output of the record into redis or some other memory store.
Next time when that record is fetched, fetch the cached store first and if it is not found then goto step 1.
If there is a cache hit, then load all the cached attributes into that model and make that model instance behave as if it were a model fetched from the database with a finite set of columns.
This is where I am stuck, what I've been doing is creating a Model.new object and setting each of the params manually. This works, but it treats the instantiated model object as a new record. There has got to be an intermediate subroutine in ActiveRecord that does the attribute setting.
I solved the problem by doing the following.
Create a new model class which extends the model class that I want to have cached into memory.
Set the table_name of the new class to the same one as the parent class.
Create a new initialize method, call the super method in it, and then allow a parameter of that method to allow for a hash variable containing all the properties of the parent class.
Overload the method new_record? and set that to false so that the associations work.
Here's my code:
class Session < User
self.table_name = 'users'
METHODS = [:id, :username] # all the columns that you wish to have in the memory hash
METHODS.each do |method|
attr_accessor method
end
def initialize(data)
super({})
if data.is_a?(User)
user = data
data = {}
METHODS.each do |key|
data[key] = user.send(key)
end
else
data = JSON.parse(data)
end
data.each do |key,value|
key = key.to_s
self.send(key+'=',value)
end
end
def new_record?
false
end
end
The memcached gem will allow you to shove arbitrary Ruby objects into it. This should all get handled for you transparently, if you're using it.
Otherwise, take a look at ActiveRecord::Base#instantiate to see how it's done normally. You're going to have to trace through a bunch of rails stack, but that's what you get for attempting such hackery!