when using find_or_create_by_name in rails, if the table is found and you pass in parameters for other attributes, will it update the table? for example:
College.find_or_create_by_name(name: 'University of Pittsburgh', calendar: 'semester')
Say the University of Pittsburgh table has already been created, but the calendar attribute is nil. Will this code update the calendar attribute to make it 'semester'?
Some context... I'm making a website with a bunch of pages for different colleges. Part of the website involves listing a bunch of data for the college. Right now I'm writing my seed file, but I anticipate having to change it. I'd like to have hundreds of schools on the website, and for each school, there are going to be hundreds of different pieces of data. I was thinking that having the seed file and using find_or_create_by_name would be a good way to do this, but if you have a better way, please let me know.
That code won't update the record if it already exists. I would suggest:
#college = College.find_or_initialize_by_name("Robot House!!")
#college.attributes = {
reputation: "partyhouse",
occupants: "robots"
}
#college.save!
You could wrap this in a method if you needed to.
find_or_create_by_ does exactly what it says: it either creates a new item (create saves to database) or finds the existing one, meaning reads it from the database.
It returns false on validation errors when creating an object
So to save changes you use the normal update methods:
if #college= College.find_or_create_by_name(given_attributes) &&
#college.update_attributes(given_attributes)
else
# handle validation errors
end
It won't hit the database twice, because update_attributes does'n apply any changes to newly created objects (but possible changes to existing ones)
To write it more explicit:
#college= College.find_or_create_by_name(given_attributes)
if #college.present?
if #college.update_attributes(given_attributes)
# do your success stuff
else
# handle update validation errors
end
else
# handle find_or_create errors
end
Related
I'm attempting to speed up some tests for a Rails controller and a bottleneck has to do with large numbers of objects being created and persisted to the database. I'm attempting to replace most of those create calls with build calls to address this.
Running Rails 5.1, and using MiniTest 5.10.3, with FactoryBot 5.0.2.
I'm attempting to go from this
#user = create(:user)
#item1 = create(:item)
#item1 = create(:item)
#transaction1 = create(:transaction, buyer: #buyer, item: #item1)
#transaction2 = create(:transaction, buyer: #buyer, item: #item2)
In this application item represents a sellable object, user represents a purchaser and transaction is the object that creates the two. The User class also has an association added to it transaction_checkout_items which returns all Transaction items which are in a state where the purchase can be completed.
So, with each test we're creating a myriad of objects and saving them all to the database. It's slow but it works. Still, I want it to be faster, so I've tried replacing the existing setup with something like this:
#user = create(:user)
def build_transaction_checkout_items(user, item)
user.transaction_checkout_items.build(attributes_for(:transaction,
buyer: user,
sale_price: item.sale_price,
item: item))
end
#item1 = build_stubbed(:item)
#item2 = build_stubbed(:item)
#transaction1 = build_transaction_checkout_items(#buyer, #item1)
#transaction2 = build_transaction_checkout_items(#buyer, #item2)
This seems to work as long as I'm in the test. If I drop a binding in my test and check the objects #user returns the user object, #user.transaction_checkout_items returns a Transaction::ActiveRecord_Associations_CollectionProxy object containing all my associated transactions, and the individual transactions have their associated items attached. However, If I put a binding.pry into the controller method which actually does the work, and look at the User object I see the correct one, but user.transaction_checkout_items now returns an empty Transaction::ActiveRecord_Associations_CollectionProxy object with nothing in it. Essentially the associations vanish, and this makes sense to me as the controller is pulling the User object from the database and going to work on it, and this new object is missing the associations. I've considered trying to stub out an any_instance method on the User class so that whenever #transaction_checkout_items is called it returns the collection of Transaction objects but I don't see any way to create a new ::ActiveRecord_Associations_CollectionProxy object. I can't simply use an array or other collection for this as there are methods on the ::ActiveRecord_Associations_CollectionProxy that need to be called for the controller logic to work.
So here I am on a Friday blocked. Is my idea of stubbing transaction_checkout_items on any User instance a good one, and if so how do I do it? Or is there an alternate strategy anyone can suggest that will allow the MiniTest stubbed associations to persist and be available when the controller code runs?
Is my idea of stubbing transaction_checkout_items on any User instance a good one, and if so how do I do it?
Stubbing out ActiveRecord methods is almost always a bad idea. It will couple your tests heavily to the implementation and potentially will make it difficult to update Rails / ActiveRecord as if anything changes in the framework your tests start breaking. There might be also lots of funny side effects you haven't thought about.
The question is also what do you actually want to test? If you start stubbing out these methods, what are you testing? In a controller / integration test, I would expect you want to test to fetch the correct records from the database.
Using build vs. create to improve test performance is a good trick but, as you already discovered, is only valuable if you use the same objects in the test. This is unfortunately not possible for integration tests and you need / should persists the records.
Or is there an alternate strategy anyone can suggest that will allow the MiniTest stubbed associations to persist and be available when the controller code runs?
I would think about why this is slow and if this is actually really a problem. How long does your test & whole test suite run or is this just a premature optimisation?
If the reason it's slow is solely because you need to create a lot of test data you could use fixtures or seed your database instead. This would both be faster than using FactoryBot although brings different issues (e.g. MysteryGuest)
Recently I had to create a couple of records in a non-rails app database table based on a previous record. It got me thinking of how would I do this in a rails app. I tried a couple of things in the Console, but nothing works.
I want to do something like this:
001> user = User.new(User.first)
I know this doesn't work but hopefully it will show you what I an thinking. User is a large table/model, and I only need to change a few fields. So, if I can set up a new record with the same values in User.first, I can then edit the fields I need to before .save-ing the record.
Thanks for any help.
I think what you want is:
user = User.first.dup
user.assign_attributes(email: "myemail#test.test")
user.save
The first line uses dup to create a copy of the object. The copy is not yet saved to the database. Replace dup with clone if you're using an old version of Rails (<3.1).
In the second line, assign_attributes alters the attributes of the object, still without saving it to the database. If you were working with an object already saved in the database, you could use update instead of assign_attributes to change the attributes of the object and save the changes in one go. That won't work here, because we haven't saved our duplicate user yet. More details on that here.
The third line finally saves the new object to the database. It saves time to just do this once, at the end.
I have a webpage that tracks budgets containing a LOT of variables, stored in 40+ columns. Over time, adjustments are made to these budgets, but I need to be able to track changes over time and year to year. I tried adding a private method to my model that should create a duplicate of the existing record triggered by a :before_update callback. However, it's not working. The update changes the existing record, and the original is not preserved at all.
Model:
class Budget < ActiveRecord::Base
before_update :copy_budget
private
def copy_budget
#budget = Budget.find(params[:id])
#budget.dup
#budget.save
end
end
I'm still learning rails, (this is in Rails 4) and I think this would have been the best way to do this. If not, is there a better way to set the form to ALWAYS post a new record instead of routing to update if a record already exists?
Currently the form_for line looks like this:
<%= form_for(#budget) do |f| %>
Everything works as it should, with the exception of the duplication not happening. What am I missing? Is it possible the .dup function is also duplicating the :id? This is assigned by auto-increment in the MySQL db I an using, so if .dup is copying EVERYTHING, is there a way to copy all of the data except the :id into a new record?
Thanks in advance for any suggestions.
the dup method returns the new object without an id, it doesn't update it in place. Since your copy_budget method is already an instance method on Budget, you also would not need to (and you wouldn't even be able to, since params aren't accessible in models) look up the budget by id and instead could just use the current instance (self). So the following changed would fix the copy_budget method for you, but you are still copying an already modified object, just before it gets saved to the database
def copy_budget
copy_of_budget = self.dup
copy_of_budget.save
end
it would work the way you're expecting it to work. However, you aren't linking the copy in anyway to the current version of the Budget (no way to tell Budget id = 1 is an older version of Budget id = 2). I'd recommend taking a look at a gem such as PaperTrail (I'm sure there are lots of others if that one doesn't suit your needs) which has already thought through a lot of the problems and features with keeping a history of record changed.
Well, not a good title but here is the problem.
[Question updated]
I have two models, Word and Definition. When the user looks up a word the definitions are enlisted and there should be a form below the definitions so that the user can contribute by adding up another definition. So far no problem. But if the search returns no result, I will ask the user to create Word along with its first definition.
I do not know how to deal with the form and logic of the problem. It is more than a nested form. Because something like form_for [#word, #word.definitions.build] do |form| would not work since there is no #word object to which the/a new definition can be referred.
Addendum:
I seem to find a way here. It just works but not so clean to me. If you think there is a better solution please share it anyway.
My approach would be to implement a form object (RailsCast). I'd use a transaction so I'm not left with any orphan database records.
In the submit method of your form object:
def submit
if dictionary_item.present?
# just save the entry
else
# start a transaction so both operations will either succeed or fail
ActiveRecord::Base.transaction do
# save the new dictionary_item
# save the entry
end
end
# return true if the objects are valid and persisted, false otherwise
end
Make sure to call save! or create! inside the transaction. The bangs are important, because an error has to be raised for the transaction to trigger a rollback.
I've come across an oddity in ActiveRecord's #relationship_ids method (that's added automatically when you declare 'has_many'), which saves immediately for existing records, which is causing me some issues, and I wonder if anyone had any useful advice.
I'm running Rails 2.3.5.
Consider this simple scenario, where an article has_many tags, say:
a = Article.first
a.name = "New Name" # No save yet
a.author_id = 1 # No save yet
a.tag_ids = [1,2,3] # These changes are saved to the database
# immediately, even if I don't subsequently
# call 'a.save'
This seems surprising to me. It's specifically causing problems whilst trying to build a preview facility - I want to update a bunch of attributes and then preview the article without saving it - but in this instance the tag changes do get saved, even though no other fields do.
(Of possible relevance is that if 'a' is a new article, rather than an existing one, things behave as I'd expect - nothing is saved until I call 'a.save')
I have a fairly nasty workaround - I can override the tag_ids= method in my model to instead populate an instance variable, and actually save the related models in a before_save callback.
But I'd love to know of a simpler way than me having to do this for every model with a has_many relationship I'd like to create a preview facility for.
Does anyone have any fixes/workarounds/general advice? Thanks!
There's a reason things are this way. It's called foreign keys. In a has many relationship, the information that links to the model that has many is stored outside of that model as a foreign key.
As in Articles, has many tags. The information that links a tag to an article is stored either in the tags table or in a join table. When you call save on an article you're only saving the article.
Active record modifies those other records immediately. Except in the case where you're working with a new article that hasn't been saved yet. Rails will delay creating/updating the associated records if it doesn't know which id to place in the foreign key.
However, if you're modifying existing records, the solution you've decided on is really all that you can do. There's an even uglier hack using accepts_nested_attributes_for, but it's really not worth the effort.
If you're looking to add this behaviour to many models but not all models, you might want to consider writing a simple plugin to redefine the assigment the method you need and add the call back in a single class method call. Have a look at the source of something like acts_as_audited to see how it's done.
If you're looking to add this behaviour to all models, you can probably write a wrapper for has_many to do that.