When using the Rails method, find_or_create_by, does it belong in the model or the controller? I am having a hard time understanding how to actually implement this method.
I want my rails application to accept JSON messages from users. The users will be sending data back to the server so it can be saved in the database. That being said, I would assume the user would have to use the 'POST' or 'PATCH method to store or update the data on my server. When I look at my routes the 'POST' method is used by the create action.
I have read the following Rails documentation but it didn't clarify anything to me. http://guides.rubyonrails.org/active_record_querying.html#find-or-create-by
Would I place the find_or_create_by method in my create action like so? Or does it belong somewhere else? It doesn't seem right in the create action...
class WifiNetworksController < ApplicationController
def create
#wifi_network = WifiNetwork.find_or_create_by(bssid: params[:bssid],
ssid: params[:ssid],
channel: params[:channel], etc...)
end
end
Ultimately I want:
Users to save new networks via JSON if it doesn't exist
Users to update existing networks via JSON if certain attributes have improved (like signal strength)
Thank you for your time!
Final Update - Thanks for the great advice everyone! I had to take a bit of everybody's advice to get it to work! Below is what I ended up doing.. Seems to work well with no errors.
def create
respond_to do |format|
if #wifi_network = WifiNetwork.find_by(bssid: wifi_network_params[:bssid])
# Logic for checking whether to update the record or not
#wifi_network.update_attributes(wifi_network_params) if #wifi_network.rssi < params[:rssi]
format.json { render :nothing => true }
else
# Must be a new wifi network, create it
#wifi_network = WifiNetwork.create(wifi_network_params)
format.json { render :nothing => true }
end
end
end
If you use strong params you can do this in your controller:
def create
#wifi_network = WifiNetwork.find_or_create_by(bssid: wifi_network_params[:bssid])
#wifi_network.update_attributes(wifi_network_params)
end
Then when a user makes a curl like:
curl -X POST localhost:3000/wifi_networks -d "wifi_network[bssid]=bssid1&wifi_network[ssid]=ssid1&wifi_network[channel]=channel1"
your create action will look up a WifiNetwork by it's bssid and update the ssid and channel attributes, or if it doesn't exist it will create a WifiNetwork with the bssid param and then update the newly created record with the rest of the atts. Be careful because if the wifi_network_params for the other attrs are empty they will update the params to nil.
I think it may be good to take a step back and really think about the interface of your application. Is there any particular reason why you need to use find_or_create_by and do everything in one controller action?
Why not simplify things and adhere to REST by having separate 'create' and 'update' actions on your WifiNetworksController:
class WifiNetworksController < ApplicationController
def create
#wifi_network = WifiNetwork.new(wifi_network_params)
if #wifi_network.save
# success response
else
# failure response
end
end
def update
# params[:id] won't work here if the client sending the request doesn't know the id of the
# wifi network, so replace it with the attribute you expect to be able to
# uniquely identify a WifiNetwork with.
if #wifi_network = WifiNetwork.find(params[:id])
# Logic for deciding whether to update or not
#wifi_network.update_attributes(wifi_network_params) if #wifi_network.signal_strength < params[:signal_strength]
else
# wifi_network not found, respond accordingly
end
end
private
# strong_parameters for Rails 4
def wifi_network_params
params.require(:wifi_network).permit(:ssid, :channel,...)
end
end
You could then have validations on your WifiNetwork model to ensure that certain attributes are unique, in order to avoid duplicates.
Or, if you really wanted to, you could combine both create and update into a single action, but create probably isn't the best name semantically.
EDIT: After your comment gave some background info, there probably isn't any benefit to using find_or_create_by, since you won't be able to tell if the record returned was 'created' or 'retrieved', which would allow you to avoid redundant update operations on it.
Assuming the bssid attribute is always a unique parameter:
def create
if #wifi_network = WifiNetwork.find(params[:bssid])
# Logic for checking whether to update the record or not
#wifi_network.update_attributes(wifi_network_params) if #wifi_network.signal_strength < params[:signal_strength]
else
# Must be a new wifi network, create it
#wifi_network = WifiNetwork.create(wifi_network_params)
end
end
Related
I've a method named update inside my DailyOrdersController:
def update
if #daily_order.update( daily_order_params.merge({default_order:false}) )
respond_or_redirect(#daily_order)
else
render :edit
end
end
My DailyOrder model:
before_save :refresh_total
def refresh_total
# i do something here
end
What I'm trying to do now is, I want the refresh_total callback to be skipped if the update request is coming from current_admin.
I have 2 user model generated using Devise gem:
User (has current_user)
Admin (has current_admin)
I try to make it like this:
def update
if current_admin
DailyOrder.skip_callback :update, :before, :refresh_total
end
if #daily_order.update( daily_order_params.merge({default_order:false}) )
respond_or_redirect(#daily_order)
else
render :edit
end
end
But it's not working and still keep calling the refresh_total callback if the update request is coming from current_admin (when the logged-in user is admin user).
What should I do now?
I think this is all what you need:
http://guides.rubyonrails.org/active_record_callbacks.html#conditional-callbacks
If you skip callback, you should enable it later. Anyway, this does not look as the best solution. Perhaps you could avoid the callbacks otherwise.
One way would be to use update_all:
DailyOrder.where(id: #daily_order.id).update_all( daily_order_params.merge({default_order:false}) )
Or you could do something like this:
#in the model:
before_validation :refresh_total
#in the controller
#daily_order.assign_attributes( daily_order_params.merge({default_order:false}) )
#daily_order.save(validate: current_admin.nil?)
or maybe it would be the best to add a new column to the model: refresh_needed and then you would conditionally update that column on before_validation, and on before_save you would still call the same callback, but conditionally to the state of refresh_needed. In this callback you should reset that column. Please let me know if you would like me to illustrate this with some code.
This may come in handy:
http://www.davidverhasselt.com/set-attributes-in-activerecord/
UPDATE
Even better, you can call update_columns.
Here is what it says in the documentation of the method:
Updates the attributes directly in the database issuing an UPDATE SQL
statement and sets them in the receiver:
user.update_columns(last_request_at: Time.current)
This is the fastest way to update attributes because it goes straight to
the database, but take into account that in consequence the regular update
procedures are totally bypassed. In particular:
\Validations are skipped.
\Callbacks are skipped.
+updated_at+/+updated_on+ are not updated.
This method raises an ActiveRecord::ActiveRecordError when called on new
objects, or when at least one of the attributes is marked as readonly.
I have a an json Api who received parameters to create a Device, like name, imei, etc. The Device can have one Blacklist object (has_one :blacklist). I would like to know what's the proper-way to create the blacklist object if a params is present in the post request of Device.
Exemple curl -X POST -d api_key=000000 -d device[name]='stack' -d device[blacklist]='true' https://www.example.com/api/devices.json
In the code for the moment I should have
def create
#device = Device.new
#device.update_attributes(strong_parameters)
if params[:device]['blacklist'] && params[:device]['blacklist'] == true
#blacklist = Blacklist.new(device_id: #device.id)
end
render :device, status: 201 # will render with jbuilder #device and #blacklist
end
But I don't like it that much :
Too much logic in one controller
Verifying parameters inside is a good practice?
If no parameters are given, how to handle the request? I know that strong parameters should return a 400, but what about #device I just created.
This controller smells for me.
Feedbacks welcome.
The result when doing a PATCH
class DevicesController
before_action :found_device, only: :blacklist # get `#device`
before_action :blacklist_device, only: :blacklist
def blacklist
render :device, status: 200
end
private
def blacklist_device
if (params[:device]['blacklisted'] and
params[:device]['blacklisted'] == true and
#blacklist = BlacklistedDevice.create(device_id: #device.id, organisation_id: current_store.organisation.id))
#device.reload
else
render json: { error: "Missing or incorrect 'blacklisted' parameter" }, status: :unprocessable_entity
end
end
end
Too much logic in the conrtoller ? No
I have also heard a lot 'too much logic in the controller is bad' but this is bullshit or rather I believe the words are not accurate enough.
What that phrase means for me, is that for example, model validations should not be in the controller, and the controller should remain light for very basic REST actions. Controller should only be a bridge between the HTML request and the model. Think of it this way : you may have several controllers modifying the same model. What you would write in EVERY controller, should most likely instead be written in the model as a validation.
But here you're dealing with specific requests (transforming a device[blacklist] == true as a Blacklist Model isn't something "natural", so yes in my opinion it should be in the controller.
Plus, a controller action of just 6 lines isn't what we could call "too much logic"
Verifying parameters inside is good Practice ? Yes/No
I assume by that you mean writing specific lines of codes in the controller like if params[xxx] == blabla or something equivalent
The way you did was good. You use specific code only for the special parameter (the blacklist) and the rest of the params go into the model as strong params, so the model validations will do the rest.
Verify parameters only if it's relevant to this particular controller (for example, if it was site-based, you could probably use a different implementation of the blacklist so the difference would have to be in the controller.
If no parameters are given, how to handle the request? I know that strong parameters should return a 400, but what about #device I just created.
This the part I don't quite like about your current implementation. You don't check for the success of your save operations. Here's what you could have written (check the result of every persistence operation result, and render appropriately)
def create
#device = Device.new
if #device.update_attributes(strong_parameters)
if (params[:device]['blacklist']
and params[:device]['blacklist'] == true
and #blacklist = Blacklist.create(device_id: #device.id))
# Handle stuff when everything is cool
render :device, status: 201 # will render with jbuilder #device and
else
# Handle stuff when there's no blacklist param true
end
else
# Handle error on model save
end
end
Inspecting params is well put in the controller - that's it's purpose - the model layer should not have knowledge of request parameters.
But you can put this info in a transient attribute with
class Device
attr_accessor 'create_blacklisted'
end
Then you can create an input field for that new attribute and an after_initialize callback in the Device model as well that can subsequently create the Blacklist entry.
I'm currently trying to implement simple audit for users (just for destroy method). This way I know if the user has been deleted by an admin or user deleted itself. I wanted to add deleted_by_id column to my model.
I was thinking to use before_destroy, and to retrieve the user info like described in this post :
http://www.zorched.net/2007/05/29/making-session-data-available-to-models-in-ruby-on-rails/
module UserInfo
def current_user
Thread.current[:user]
end
def self.current_user=(user)
Thread.current[:user] = user
end
end
But this article is from 2007, I'm not sure will this work in multithreaded and is there something more up to date on this topic, has anyone done something like this lately to pass on the experience?
Using that technique would certainly work, but will violate the principle that wants the Model unaware of the controller state.
If you need to know who is responsible for a deletion, the correct approach is to pass such information as parameter.
Instead of using callbacks and threads (both represents unnecessary complexity in this case) simply define a new method in your model
class User
def delete_user(actor)
self.deleted_by_id = actor.id
# do what you need to do with the record
# such as .destroy or whatever
end
end
Then in your controller simply call
#user.delete_user(current_user)
This approach:
respects the MVC pattern
can be easily tested in isolation with minimal dependencies (it's a model method)
expose a custom API instead of coupling your app to ActiveRecord API
You can use paranoia gem to make soft deletes. And then I suggest destroying users through some kind of service. Check, really basic example below:
class UserDestroyService
def initialize(user, destroyer)
#user = user
#destroyer = destroyer
end
def perform
#user.deleted_by_id = #destroyer.id
#user.destroy
end
end
UserDestroyService.new(user, current_user).perform
I'm using service objects to abstract a stripe payment feature into it's own class. I'm using this method https://gist.github.com/ryanb/4172391 talked about by ryan bates.
class Purchase
def initialize(order)
#order = order
end
def create_customer
customer = stipe create customer
if customer
charge_customer(customer)
else
stipe error in creating customer
end
end
def charge_customer(customer)
if charge is sucessfull
update_order(charge details)
else
stripe error in charging card
end
end
def update_order
#order.update(payment attributes)
end
end
Then in the order controller i'm doing something like
def create
#order = Order.new(params[:order])
if #order.save
payment = Payment.new(#order)
else
render 'new' with flash message "payment error"
end
end
My question is, how do i get the stipe error messages("stipe error in creating customer" and "stripe error in charging card") to display to the user?
Or can i call a service object in the order model and add it to order error messages? For example,
Order controller
#order.save_with_payment
Order model
def save_with_payement
payment = Payment.new(self)
#update order
self.payment_token = payment.token
etc
end
If i can do that with the model, how to i make a validation that shows the stripe errors?
Thanks in advance.
First of all try to separate concerns as possible. It already feels like Your Purchase/Payment class is doing too much, probably it should delegate part of it's routines to other service objects.
Second, I agree with phoet. I don't see the reason why You wouldn't pass params hash to service object. In our latest project we fully rely on service objects we call Factories to produce/manipulate our model objects. In Your case You could do like this:
class OrderFactory
def self.create(params)
new(params).create
end
def initialize(params)
#order = Order.new(params[:order])
end
def create
#payment = Payment.new(self)
#order.payment_token = payment.token
#....
end
end
Talking about validations - You can add validation methods to plain Ruby objects. For example by using ActiveModel:
http://yehudakatz.com/2010/01/10/activemodel-make-any-ruby-object-feel-like-activerecord/
Then You can create custom validators, like Ryan suggested.
Or You can use a gem like Virtus and add some custom validation rules.
i think that the service should be responsible for handling all this. so it might look like this
def create
#payment = Payment.new(params[:order])
if #order = #payment.execute_transaction
[...]
else
[...]
end
end
so the payment service would handle creating and persisting the order and also might be adding validation errors. you could also return some kind of #result object that might consist of the order and depending error messages.
I asked a very similar question just one day ago. The response I received was very helpful indeed (credit to #gary-s-weaver) so take a look.
Rails generic errors array
I went with the rescue_from approach in the end and it works well for me. Let me know how you get on as I'm very interested in this area. And if you need any code samples give me a shout.
I've written a gem for service objects. Check out peafowl gem.
I have a resource in my project that collects some information from a user. Basically it's a form that they fill out before they can access another area of the site. It then sets a cookie for a week, but if they come back it will look up their previous entry and keep their preferences tied to them (and will update any details as long as the email address matches).
Currently I have a Applicants controller that looks like this:
class ApplicantsController < ApplicationController
...
def create
#applicant = Applicant.find_or_initialize_by_email(params[:applicant])
if #applicant.new_record? ? #applicant.save : #applicant.update_attributes(params[:applicant])
set_cookie_and_redirect
else
render 'new'
end
end
def update
if #applicant.update_attributes(params[:applicant])
set_cookie_and_redirect
else
render 'new'
end
end
end
The set_cookie_and_redirect is a private method that just sets some cookies and redirects the user to a page. The code works, but it just feels dirty. It's essentially updating a record within the create method under the condition that it's not a new record. I'm also forced to have an update method in case an existing record comes back with a validation error--the form helper will then switch the form over to sending to the update method.
So to my point... is there a more appropriate way to push the update_attributes call in the create method to the update method? Or better put, is there a better way to respect the RESTful methods in isolating the create and update functionality?
UPDATE: I wanted to be a little more specific too. If the user has filled this form out before it will set a cookie so they don't have to fill it out again for seven days. However after seven days the cookie is expired and they see the form again. The controller doesn't know if the user is new or existing until they add user input into the form which is then compared based on the email address.
Thanks in advance! I definitely look forward to anyone's thoughts on this.
The create method should only create, and the update method should only update. Let Rails decide which is going to happen based on what is inside of #applicant when the form is rendered - It essentially does what you're doing: Checks if the record is new or not, and sends it to update/create accordingly. Example:
def applicant
#applicant = Applicant.find_or_initialize_by_email(cookies[:email])
# renders applicant.html.erb form
end
<%= form_for #applicant do |f| %>
# ... fields ...
<% end %>
def create
#applicant = Applicant.new(params[:applicant])
#applicant.save
# .. etc.
end
def update
#applicant = Applicant.find_by_email(cookies[:email])
#applicant.update_attributes(params[:applicant])
# ... etc.
end
Rails will send the request to the correct action based on the new_record? status of the Applicant object.