So imagine you have 2 models, Person and Address, and only one address per person can be marked as 'Main'. So if I wanna change a person's main address, I need to use a transaction, to mark the new one as main and unmark the old one. And as far as I know using transactions in controllers is not good so I have a special method in model, thats what I've got:
AddressesController < ApplicationController
def update
#new_address = Address.find(params[:id])
#old_address = Address.find(params[:id2])
#new_address.exchange_status_with(#old_address)
end
end
Model:
class Address < ActiveRecord::Base
def exchange_status_with(address)
ActiveRecord::Base.transaction do
self.save!
address.save!
end
end
end
So thequestion is, if the transaction in the model method fails, I need to rescue it and notify the user about the error, how do I do that? Is there a way to make this model method return true or false depending on whether the transaction was successful or not, like save method does?
I probably could put that transaction in the controller and render the error message in the rescue part, but I guess its not right or I could put that method in a callback, but imagine there is some reason why I cant do that, whats the alternative?
PS dont pay attention to finding instances with params id and id2, just random thing to show that I have 2 instances
def exchange_status_with(address)
ActiveRecord::Base.transaction do
self.save!
address.save!
end
rescue ActiveRecord::RecordInvalid => exception
# do something with exception here
end
FYI, an exception looks like:
#<ActiveRecord::RecordInvalid: Validation failed: Email can't be blank>
And:
exception.message
# => "Validation failed: Email can't be blank"
Side note, you can change self.save! to save!
Alternate solution if you want to keep your active model errors:
class MyCustomErrorClass < StandardError; end
def exchange_status_with(address)
ActiveRecord::Base.transaction do
raise MyCustomErrorClass unless self.save
raise MyCustomErrorClass unless address.save
end
rescue MyCustomErrorClass
# here you have to check self.errors OR address.errors
end
Related
Let's imagine I have a class
class Test < ActiveRecord::Base
include AuthenticatorHelper
def test
authenticate_or_fail!
puts "If I fail, this should be unreachable"
end
end
and
module AuthenticationHelper
def authenticate_or_fail!
#user = User.find(params[:token])
unless #user
render :json => {code: 401, :err => 'Unauthorized'} and return
end
end
end
What I want to do is either authenticate or reply with a json msg. However, it will obviously ignore my return statement due to nesting and it will always print my message
If I fail, this should be unreachable
Regarding the question
You could extract the call into a before_filter/before_action (based on the rails version).
class Test < ActiveRecord::Base
include AuthenticatorHelper
before_action :authenticate_or_fail!
def test
puts "If I fail, this should be unreachable"
end
end
Please see the documentation for further details.
Because your helper method renders in case of a failure, rails will prevent the test method to be called. You will not need the and return part then, which would only have returned from the method anyway and as such was a NoOp.
Apart from the question but also noteworthy:
I don't want to point out errors for the sake of it. I just want to prevent the OP from running into a series of bugs later on.
User.find(params[:token])
Will raise an exception if no record is found. Because of that, the unless #user part will not be evaluated in case of an invalid token. You could use
User.find_by(id: params[:token])
instead.
Your class which looks like it acts as a controller is named Test and inherits from ActiveRecord::Base. The first is unusual as TestsController would be more along the lines of rails and the seconds looks plain wrong. A controller has to inherit from ApplicationController (which itself inherits from ActionController::Base)
Right now I have a model Trip that when saved, runs some callbacks. I want to isolate this behavior so that it happens only when it runs on the controller (create/update/destroy), so I want to remove the callbacks.
Assuming I have a service object DoSomething#call which accepts trip and does everything I need, what are my options to run it in create/update/destroy?
I have a few ideas but they involve horror things like:
def create
super() do |success, failure|
#action_successful = failure.instance_of?(
InheritedResources::BlankSlate
) || failure.class.nil?
if #action_successful
DoSomething.call(Trip.find(params[:id]))
end
end
end
Which comes with a lot of downsides:
The horrible way to detect if an action is successful
No way to get the in-memory reference to the object being acted on (reload from db)
Since I have no reference to the in memory object, it's quite problematic to run something during on destroy (no reference, can't reload)
Additional code as requested
class Trip
end
The custom service (I've multiples)
class SaveLastChangedTrip
def call(user, trip)
return if user.nil?
user.update_attributes!(last_trip: trip)
end
end
and the activeadmin file
ActiveAdmin.register Trip do
controller do
def update
if super() # This is pseudocode, I want to run this branch only if save is successful
SaveLastChangedTrip.call(current_user, resource)
end
end
end
end
I think you are looking for something like this:
def create
create! do |success, failure|
success.html do
DoSomething.call(resource)
end
end
end
See comments.rb for similar example.
So I apologize for how noobish these questions may seem. I'm new to rails and as a first task I also brought in Neo4J as it seemed like the best fit if I grow the project.
I'll explain the flow of actions then show some example code. I'm trying to add in step 3-5 now.
User logs in via FB
The first login creates a user node. If the user exist, it simply retrieves that user+node
After the user node is created, the koala gem is used to access the FB Graph API
Retrieves friendlist of each friend using the app.
Go through each friend and add a two way friendship relationship between the two users
As 3-5 only needs to happen when the user first joins, I thought I could do this in a method associated with after_save callback. There is a flaw to this logic though as I will need to update the user at some point with additional attributes and it will call after_save again. Can I prevent this from occurring with update?
SessionsController for reference
def create
user = User.from_omniauth(env["omniauth.auth"])
session[:user_id] = user.id
redirect_to root_url
end
def destroy
session.delete(:user_id)
redirect_to root_path
end
So in my user.rb I have something like this
has_many :both, :friendships
after_save :check_friends
def self.from_omniauth(auth)
#user = User.where(auth.slice(:provider, :uid)).first
unless #user
#user = User.new
# assign a bunch of attributes to #user
#user.save!
end
return #user
end
def facebook
#facebook ||= Koala::Facebook::API.new(oauth_token)
block_given? ? yield(#facebook) : #facebook
rescue Koala::Facebook::APIError => e
logger.info e.to_s
nil
end
def friends_count
facebook { |fb| fb.get_connection("me", "friends", summary: {}) }
end
def check_friends(friendships)
facebook.get_connection("me", "friends").each do |friend|
friend_id = friend["id"]
friend_node = User.where(friend_id)
Friendship.create_friendship(user,friend_node)
return true
end
end
friendship.rb
from_class User
to_class User
type 'friendship'
def self.create_friendship(user,friend_node)
friendship = Friendship.create(from_node: user, to_node: friend_node)
end
I'm not sure if I'm on the right track with how to create a relationship node. As I just created #user, how do I incorporate that into my check_friends method and retrieve the user and friend node so properly so I can link the two together.
Right now it doesn't know that user and friend_user are nodes
If you see other bad code practice, please let me know!
In advance: Thanks for the help #subvertallchris. I'm sure you will be answering lots of my questions like this one.
This is a really great question! I think that you're on the right track but there are a few things you can change.
First, you need to adjust that has_many method. Your associations always need to terminate at a node, not ActiveRel classes, so you need to rewrite that as something like this:
has_many :both, :friends, model_class: 'User', rel_class: 'Friendship'
You'll run into some problems otherwise.
You may want to consider renaming your relationship type in the interest of Neo4j stylistic consistency. I have a lot of bad examples out there, so sorry if I gave you bad ideas. FRIENDS_WITH would be a better relationship name.
As for handling your big problem, there's a lot you can do here.
EDIT! Crap, I forgot the most important part! Ditch that after_save callback and make the load existing/create new user behavior two methods.
class SessionsController < ApplicationController
def create
user = User.from_omniauth(env["omniauth.auth"])
#user = user.nil? ? User.create_from_omniauth(env["omniauth.auth"]) : user
session[:user_id] = #user.id
redirect_to root_url
end
def destroy
session.delete(:user_id)
redirect_to root_path
end
end
class User
include Neo4j::ActiveNode
# lots of other properties
has_many :both, :friends, model_class: 'User', rel_class: 'Friendship'
def self.from_omniauth(auth)
User.where(auth.slice(:provider, :uid)).limit(1).first
end
def self.create_from_omniauth(auth)
user = User.new
# assign a bunch of attributes to user
if user.save!
user.check_friends
else
# raise an error -- your user was neither found nor created
end
user
end
# more stuff
end
That'll solve your problem with getting it started. You may want to wrap the whole thing in a transaction, so read about that in the wiki.
But we're not done. Let's look at your original check_friends:
def check_friends(friendships)
facebook.get_connection("me", "friends").each do |friend|
friend_id = friend["id"]
friend_node = User.where(friend_id)
Friendship.create_friendship(user,friend_node)
return true
end
end
You're not actually passing it an argument, so get rid of that. Also, if you know you're only looking for a single node, use find_by. I'm going to assume there's a facebook_id property on each user.
def check_friends
facebook.get_connection("me", "friends").each do |friend|
friend_node = User.find_by(facebook_id: friend["id"])
Friendship.create_friendship(user,friend_node) unless friend_node.blank?
end
end
The create_friendship method should should return true or false, so just make that the last statement of the method does that and you can return whatever it returns. That's as easy as this:
def self.create_friendship(user, friend_node)
Friendship.new(from_node: user, to_node: friend_node).save
end
create does not return true or false, it returns the resultant object, so chaining save to your new object will get you what you want. You don't need to set a variable there unless you plan on using it more within the method.
At this point, you can easily add an after_create callback to your ActiveRel model that will do something on from_node, which is always the User you just created. You can update the user's properties however you need to from there. Controlling this sort of behavior is exactly why ActiveRel exists.
I'd probably rework it a bit more, still. Start by moving your facebook stuff into a module. It'll keep your User model cleaner and more focused.
# models/concerns/facebook.rb
module Facebook
extend ActiveSupport::Concern
def facebook
#facebook ||= Koala::Facebook::API.new(oauth_token)
block_given? ? yield(#facebook) : #facebook
rescue Koala::Facebook::APIError => e
logger.info e.to_s
nil
end
def friends_count
facebook { |fb| fb.get_connection("me", "friends", summary: {}) }
end
end
# now back in User...
class User
include Neo4j::ActiveNode
include Facebook
# more code...
end
It's really easy for your models to become these messy grab bags. A lot of blogs will encourage this. Fight the urge!
This should be a good start. Let me know if you have any questions or if I screwed anything up, there's a lot of code and it's possible I may need to clarify or tweak some of it. Hope it helps, though.
I recently had a rails model that had several callbacks on it like so:
class Model < ActiveRecord::Base
before_validation :fetch_posts
after_create :build_posts
def fetch_posts
fetch_collection
rescue MyException => e
self.errors.add(:post, e.message)
end
def build_posts
fetch_collection.each do |item|
DifferentModel.build(item)
end
end
def fetch_collection
#collection ||= method_that_fetches_collection_from_external_source
end
end
This was working just fine but it was making it extremely difficult to write tests, as whenever I wanted to create a Model I had to stub out all the callbacks. Enter service objects:
class ModelFetcher
attr_reader :model
def initialize(model)
#model = model
end
def save
model.fetch_posts
if model.save
model.build_posts
return true
else
return false
end
end
end
The problem I'm seeing now, in the case where a model does indeed contain an error (from the fetch posts method), it doesn't get carried over to the model.save call in the SO. That is to say, the Model.new has an error, but once I call .save on that Model.new it doesn't maintain the error and the model saves properly.
I considered adding validate :fetch_posts but then I am back in the same situation I was before as this is essentially a callback.
Any advice on how to structure this better? Is it possible to maintain an error from Model.new to .save? Am I fundamentally misunderstanding something?
Thanks!
Here is an alternate solution which is to overwrite run_validations! since you have none.
class Model < ActiveRecord::Base
after_create :build_posts
def fetch_posts
fetch_collection
rescue MyException => e
self.errors.add(:post, e.message)
end
def build_posts
fetch_collection.each do |item|
DifferentModel.build(item)
end
end
def fetch_collection
#collection ||= method_that_fetches_collection_from_external_source
end
private
def run_validations!
fetch_posts
errors.empty?
end
end
Usually this method looks like
def run_validations!
run_callbacks :validate
errors.empty?
end
but since you have no validations it should serve a similar purpose on #save.
Or as I suggested in a comment you can replace save with model.errors.any? Since save will clear your original errors set by fetch_posts but errors.any? Will check if there were errors during the fecth_posts method.
For a specific role (group of users) I added the :readonly to every find on the active record
def self.find(*args)
if User.current_user.has_role? 'i_can_only_read'
with_scope({:find => {:readonly => true}}) do
result = super *args
end
end
end
Of course it raises now ActiveRecord::ReadOnlyRecord Exceptions in Controller passed on to the user; not very nice.
Can I catch this type of error in one place? Like in production.rb or in the application.rb? Or can I configure a specific error page for this error?
Yes, simply override rescue_action_in_public like this:
class ApplicationController < ActionController::Base
...
def rescue_action_in_public(exception)
case exception
when ActiveRecord::ReadOnlyRecord
# DO SOME LOGIC HERE
end
end
end
end
This will execute your action when in "production", but leave you with an informative stack trace when you are in "development".
Rails has a number of other rescue_action_* methods that might be more suitable to your problem...take a look at http://api.rubyonrails.org/classes/ActionController/Rescue.html