RAILS 3 - Transactions in controllers - ruby-on-rails-3

I have an example Action in a Controller.
def some_action
product = Product.new
product.name = "namepro"
if product.save
client.update_attribute(:product_id,product.id)
end
end
How to add transactions for this code? I try with this example code:
def some_action
**transaction do**
product = Product.new
product.name = "namepro"
if product.save
client.update_attribute(:product_create,Time.now)
end
**end**
end
But it produces this error:
undefined method `transaction'
I read about using transactions in Controllers is a bad practice but I don't know why is the reason (http://markdaggett.com/blog/2011/12/01/transactions-in-rails/)
In the example, if product has been created and saved and the client update fail... Rails must not do nothing.
thanks.

You can use a transaction in a controller if you really want to. As you noted, it's bad practice, but if you want to do it, just call Product.transaction do instead of transaction do. transaction is a class method on ActiveRecord::Base, so you need to call it on an ActiveRecord-derived class. Any model class in your application will do (nit-picking caveat: if you are connecting to different databases for different models, that may not be true...but you're probably not doing that).
The reason this is a bad practice is that it doesn't properly separate concerns according to the MVC paradigm. Your controller shouldn't be so concerned with your data persistence implementation. A better approach would be to add a method to Product. Maybe something like this:
def save_and_update_create_time
transaction do
if save
client.update_attribute(:product_create, Time.now)
end
end
end
Then instead of calling product.save in your controller, call product.save_and_update_client_create_time. You may need to pass client to that method too; it's unclear from your code where client comes from. If it's an attribute on product, then the method above should work.
There are better, more Railsy ways to do this, too, especially if a product knows about its client without needing any controller data. Then you can just use an after_save callback, like this (add to Product class):
after_save :update_client
private
def update_client(product)
product.client.update_attribute(:product_create, Time.now)
end
Then every time a Product is saved, the field on the associated client will be updated. You'll possibly have to introduce some code to check for the existence of a client first.
The benefit to using callbacks, besides cleaner code, is that the entire callback chain runs in a single transaction along with the save; you don't need to create the transaction manually. You can read more about callbacks in the Rails documentation.

Related

In a Rails ActiveRecord model, is using after_initialize callbacks a very bad idea?

Let's suppose we have this model
class Account < ActiveRecord::Base
after_initialize :set_name
def set_name
self.name = ‘My Account’
end
end
Now I want run a query that returns only some attributes of the model but not all of them, in particular is not returning the "name" attribute that it is used in after_initialize callback
Account.group(:name).select("count(*), id").first
And then this execution raises the following error because the set_name callback uses an attribute that has not been "loaded" or selected into the records returned by the query.
ActiveModel::MissingAttributeError: missing attribute: name
Fortunately for some particular cases I can execute the same sql query without using the Account model at all to get the desired result
sql = Account.group(:name).select("count(*), id").to_sql
ActiveRecord::Base.connection.execute(sql).first
=> #<Mysql2::Result:0x00000106eddbc0>
But the point is, what if I want to get Account objects instead of a Mysql2::Result one? Should the .select method return "complete" objects with all their attributes (e.g. filling the missing columns with Nil's)? Or is just a very bad idea to use after_initialize callbacks for our ActiveRecord models? Of course we can also add some code in the callback to check if the property exists or not but, in my opinion, this is unnatural or sounds weird working in an OO language.
Most uses of after_initialize can be (and SHOULD be) replaced with defaults on the corresponding database columns. If you're setting the property to a constant value, you may want to look into this as an alternative.
EDIT: if the value isn't constant, a call to has_attribute?(:name) will guard against this error - ActiveModel::MissingAttributeError occurs after deploying and then goes away after a while
No, it is not a bad idea, in fact I use it very often at work. The valid use case for this would be when you want code to run before you try and do anything with the object. Here is a breakdown of some of the filters offered.
# Before you intend to do anything with the object
after_initialize
# Before you intend to save the object
before_save
# After you've saved the object
after_save
# Before you save a new record
before_create
# After you create a new object
after_create

Rails 3 Validation that only gets triggered once to simulate a warning on new records

I was wondering what the best implementation for displaying a warning for a particular field being sent to the database.
To give you an example, somebody provides data which is considered valid, but questionable. So we want to treat it as if it was a regular validation error on the first go and confirm that it's what the user actually wants to enter. At this point they will have the option to either continue or change the data being entered. If they choose to continue they'll be given the go-ahead and we'll skip that validation on the next run-through.
However (and this is the part I'm not sure about), if they change that field to another value that can be considered questionable we want to take them through the same process. Keep in mind these are new records and not records that have already been persisted to the database.
Can such a feat be accomplished with basic conditional validations? Would there be a better option?
Just to clarify my application knows exactly how to handle this questionable data, but it's going to be processed differently than normal data and we just want to inform the user ahead of time with a warning.
Currently the validation is your typical custom validation method that dictates the validity of an object.
validate :some_field_some_rules
def some_field_some_rules
if some_conditions_must_be_true
errors.add(:some_field, "warning message")
end
end
Edited, let's try with a custom validation that will be triggered only when you need to.
class MyModel < ActiveRecord::Base
attr_accessor :check_questionable
validate :questionable_values_validation, on: :create, if: Proc.new { |m| m.check_questionable }
def initialize
check_questionable = true
end
private
def questionable_values_validation
if attribute1 == "Questionable value"
self.errors[:base] << "Attribute1 is questionable"
check_questionable = false
end
end
end
Then, when you render the create form, be sure to add an hidden_field for check_questionable :
f.hidden_field :check_questionable
So the first time, when calling the create action, it'll save with check_questionable = true. If there's a questionable value, we add an error to ActiveRecord standard errors AND set the check_questionable to false. You'll then be re-rendering the new action but this time with the hidden_field set to false.
This way, when the form is re-submitted, it won't trigger questionable_values_validation ...
I didn't test it, might need some tweak, but it's a good start I believe!

How to get deeply nested errors to get to my REST API?

First, some background:
I have a Company model, a Project model and a Task model. A Project belongs to a company and a Task belongs_to a Project.
The Project model holds several attributes: company_id, date. These attributes uniquely identify a project
I am letting the users create a task by API by POSTing to a URL that contains the details necessary to identify the Project. For example:
POST /projects/<comnpany_name>/<date>/tasks/
In order to make life easier for the users, in case there is no project with the given details, I'd like to create the project on the fly by the given details, and then to create the task and assign it to the project.
...And my problem is:
When there is a problem to create the project, let's say that the company name is not valid, what is the right way to return the error message and communicate to the user?
I'll explain what I mean: I added a create_by_name_and_company_name method to the Project:
def self.create_by_name_and_company_name(name, company_name)
if company = Company.find_by_name(company_name)
project = Project.create(company_id: company.id,
name: name)
else # cannot create this project, trying to communicate the error
project = Project.new(name: name)
project.errors.add(:company, 'must have a valid name')
end
company
end
I was hoping that by returning an unsaved Company object, with errors set, will be a good way communicate the error (This is similar to how rails work when there's a validation error).
The problem is that when calling valid? on the company object, it removed the error I wrote there and adds the regular validation errors (in this case, company can't be blank).
And a bonus question...
And there is a conceptual problem as well: since I'm creating a model by providing parameters that are being used to create the actual attributes, they doesn't always map nicely to the errors[:attr] hash. In this case it is not so bad and I'm using the company field for the company name parameter, but I guess this can get messier when the parameters provided to the create method are less similar to the model attributes.
So what is the preferred approach to tackle that problem? Is there something basically wrong with that approach? if so, what is the preferred approach?
About overriding the default rails validation error message, you need to write your validation constraint like this:
validates_presence_of :name, :message => "must be a valid name"
I figure that it is best to avoid such nesting and stick to a shallower API.

Querying another model from inside a model created by FactoryGirl

Just starting with FactoryGirl. I have a Model named Subscription. It has a method 'set_price` which apparently does some calculations. In order to do so, it has to ask another model for some values:
def set_price
base_price = Option.find_by_key(:base_price).value.to_f
# […] some calculations
end
When running my specs I get:
NoMethodError:
undefined method `value' for nil:NilClass
Which is quite logical since I didn't (yet?) create any Options.
Is FactoryGirl suited for this? Do I have to create Option fixtures in this case? Or just mock it?
This will fail because there are no Options in the database. You can either create the option factory before calling set_price in the test (you'll need to make sure find_by_key(:base_price) will return your factory created option in this case), or you can as you say use a mock:
option = mock_model('Option', :value => 1)
Option.stub(:find_by_key).and_return(option)
The mock has the advantage that it will not touch the database, but it's potentially more brittle.

Rails 3 Shopping Cart Design Questions

I have a Transactions object (as a part of a shopping cart) that belongs_to two other objects, Products and Services. Both Products and Services are nested with Transactions to create URLs like /products/1/transactions/new and /services/1/transactions/new. And, the forms are created using form_for [#product, #transaction] do |f| type formats. Note: Products and Services are too different in design and functionality to combine them into a STI-type single object.
My question is: Is there a better way of doing this without STI? And, what's the best way to check in the controller to determine the type of object to act on?
IE in the new action:
if ???
#product = Product.find(params[:product_id])
#transaction = #product.transactions.build
elsif ???
#service = Service.find(params[:service_id])
#transaction = #service.transactions.build
end
On a related note, does anyone know of any tutorials that discuss shopping cart design for Rails 3? I've seen a few in books, but they use sessions to store the entire cart object, which isn't very secure, in my opinion. And, others are overly simplistic.
Any help would be greatly appreciated!
I think you have two basic options here.
(1) Create two transaction types/models: product_transactions and service_transactions. Both can inherit from a common transaction module. This approach allows you to ignore the issue of which, "which kind of transaction am I dealing with?" and focus on the common implementation details in the transaction module. You can then maintain two simpler nested controllers, /products/<id>/transactions and /services/<id>/transactions that don't require type checking.
(2) Move the type checking into the common transaction model. This approach assumes that interactions between the transaction and product or service will be handled via the transaction module, so it won't be your responsibility to know which one you are interacting with. For example:
class Transaction
belongs_to :service
belongs_to :product
def parent
#parent ||= product || service
end
def call_some_action_on_parent
parent.some_action
end
end
You can do something similar in your controller:
#parent = params[:service_id].blank? ? Product.find(params[:product_id]) : Service.find(params[:service_id])
#transaction = #parent.transactions.build(params[:transaction])
The option you settle on should really be a decision based on your needs surrounding the transaction object. If there is a lot of custom code depending on if you are interacting with a product or service you should follow the first approach. If the code is essentially the same you should take the second approach and focus on the contract between a transaction and its parent object (product, service or other) and ignore what the object type is. In essence you are really only interested in whether or not the parent object responds to specific methods, not what kind of object it actually is. As a general rule, when possible, avoid type checking and focus on the responds_to? method instead.