Topic: How to use dynamic finders?

I'm new to rails, but I'm building a baking journal app and having issue with my Ingredient model as whenever I input an ingredient on the entry form I'm creating a new ingredient each time.  What I want to do is check if the ingredient table already has that ingredient, and if so to link it, if not, create it.  So for example, I create a new Pizza entry, and I type "flour", "water", "salt" for the ingredients.  All these records already exist in my ingredients table, so I want to just reference those instead of creating a new one.  I heard that dynamic finders would do what I want, but I'm not able to get that to work as I don't know how to access it from the form...  This is my entries controller new action where I define a dynamic finder:

  def new
    @entry = Entry.new
    @entry.entry_ingredients.build.build_ingredient
    @entry.steps.build
    
    @ingredient = Ingredient.where(params[:ingredient]).first_or_create()

    respond_to do |format|
      format.html # new.html.erb
      format.xml  { render :xml => @entry }
    end
  end

My models accept nested attributes so that I'm able to submit entries and ingredients and steps etc from all the same form.  Here are my relevant models:

class Entry < ActiveRecord::Base
  belongs_to :user
  has_many :entry_ingredients
  has_many :ingredients, :through => :entry_ingredients
  has_many :steps
  
  accepts_nested_attributes_for :entry_ingredients, :allow_destroy => true
  accepts_nested_attributes_for :steps, :reject_if => lambda { |s| s[:description].blank? }, :allow_destroy => true
end

class Ingredient < ActiveRecord::Base
  has_many :entry_ingredients
  has_many :entries, :through => :entry_ingredients
  
end

class EntryIngredient < ActiveRecord::Base
  belongs_to :entry
  belongs_to :ingredient
  belongs_to :unit

  accepts_nested_attributes_for :ingredient, :reject_if => lambda { |a| a[:name].blank? }, :allow_destroy => true

end

and here is my view code, this resides within a fields_for :entry_ingredients on the _form partial:

<li>
  <%= f.fields_for :ingredient do |builder| %>
    <%= builder.label :name %>
    <%= builder.text_field :name, :class => "ingredient_field" %>
  <% end %>
  <%= f.label :quantity %>
  <%= f.text_field :quantity, :class => "ingredient_quantity" %>
  <%= f.label :unit_id, "Unit" %>
  <%= f.select :unit_id, Unit.all.map { |u| [u.name, u.id] }, { :prompt => "Please Select" }, { :class => "ingredient_unit" } %>
  <%= f.hidden_field :_destroy, :class => "delete_ingredient" %>
  <%= link_to "Remove", "#", :class => "remove_fields" %>
</li>

How do I get this to work?  Can anyone point me in the right direction?

Re: How to use dynamic finders?

I never tried this myself, so I'd appreciate you provide the running project so I could tinker with it to find out what exactly is going on (upload code to github or an archive of your folder somewhere)

Re: How to use dynamic finders?

Sure thing, heres the repo https://github.com/technomad/baking_journal

you can find the changes I was trying in the branch "ingredients"

Re: How to use dynamic finders?

Ok, I skimmed the source and tested how it works.  Thanks.  I'm going to follow your progress if you push changes to the repo.

My first guess would be to use a rails3-enabled auto-completion gem like.  Seems to do a lot behind the scenes, but form generation with rails3-autocomplete & formtastic looks neat.  Give it a shot, I'd say!

Other feedback:

  • You're JS triggers for "Add Ingredient" seem a bit unorthodox smile  I'd rather use rails.js/UJS and `link_to :remote => true` to render ingredient list items.  This way you could refactor the ingredient form fields to use a partial both in the initial form setup and when a new ingredient row is requested.

  • Optionally, you could look into using a Form Object as well to put pulling associations and form data together in another place:  you can use an object that accepts exactly what you'd like, and only on form submission would you have to use and create your usual ActiveRecord model objects.  Confer the following posts:
    http://blog.codeclimate.com/blog/2012/1 … rd-models/
    http://robots.thoughtbot.com/post/33296 … rm-objects

Re: How to use dynamic finders?

@DivineDominion  Thanks!  Yeah I know my JS is very unorthodox.  I was trying to follow ryan bates railscast on nested forms and I couldn't get the add field to work because of some error, so i decided to roll my own for now.

I'll look into those auto-completion gems right now, but I'm new to all this programming and so I prefer not to use gems that do magic haha

Re: How to use dynamic finders?

I can relate to your decision. smile

I tried the rails3-autocomplete example myself just now.  The way to reference ingredients would be to do something like this:

<input type="hidden" name="foo" value="" id="ingredient_number_X_id">
<%= autocomplete_field_tag 'name', '', welcome_autocomplete_brand_name_path, :id_element => "#ingredient_number_X_id" %>

The hidden field is set upon selecting an entry from the auto-complete widget.

This way, though, people could still enter an ingredient and _not_ select the suggestion from the list.  If the auto-completion widget isn't used, the ID won't be set.  To me it seems you'd fare better with a combined approach:

  • Optional: offer auto-completion on the form for user's convenience.  Don't set a hidden ID field to build the association but simply insert a canonical ingredient name this way.

  • Check ingredient server-side and build associations manually via `first_or_initialize`.

Also I wonder why entries#new uses this:

@ingredient = Ingredient.where(params[:ingredient]).first_or_create()

Since a plain GET request won't have any information in params[:ingredient], wouldn't you create a new Ingredient in the database on every GET request?

Re: How to use dynamic finders?

Thanks for looking so deep into this!

You're absolutely right, and I do want to eventually implement an auto-complete feature so that users can select from a pre-defined list of ingredients in addition to being able to create their own.  However, I'm trying to learn this step by step, and for me the next part of this is to figure out how to stop duplicating the ingredients. 

The table only has 'name' as an attribute, but whenever a form is submitted, a new ingredient is created.  This is why I wanted to use dynamic finders, I was told it would be the way to stop creating ingredients on each form submit if the ingredient already existed as it would find the ingredient by name or if it returns nil it creates it.  But I'm not sure how to use the dynamic finders...  you actually pointed out my attempt at using it in your last comment about the entries#new action, I just put it there and didn't really know what to do with it haha.  Where would you put this code?  I don't have an ingredients controller and so thats why I was trying to find a place for it in the entries controller, but I'm starting to think the best place for it to go would be in the ingredients controller.

This is the logic that should happen:

  • User clicks to create a new entry

  • User types multiple ingredients into the entry form

  • On form submit, find if ingredients exist in table, the ones which do, assign their id to ingredient_id in entry_ingredients table.  For the ones that don't, create them and assign their id to ingredient_id in entry_ingredients.

Last edited by gih (2013-04-02 20:37:58)

Re: How to use dynamic finders?

Okay, I assume your rationale was like this:  in entries#new, I have to prepare an @entry=Entries.new object so the form knows where it belongs to.  Therefore, I have to prepare an Ingredients object, too.

Thing is, you're going to create (and store in your database) at least one empty ingredient object this way, maybe only on your first request, but that's one object too much nevertheless.  This it what the server log on the console reveals with a fresh database:

Started GET "/entries/new" for 127.0.0.1 at 2013-04-03 11:19:55 +0200
Processing by EntriesController#new as HTML
  Ingredient Load (0.2ms)  SELECT "ingredients".* FROM "ingredients" LIMIT 1
   (0.0ms)  begin transaction
  SQL (5.0ms)  INSERT INTO "ingredients" ("created_at", "name", "updated_at") VALUES (?, ?, ?)  [["created_at", Wed, 03 Apr 2013 09:19:55 UTC +00:00], ["name", nil], ["updated_at", Wed, 03 Apr 2013 09:19:55 UTC +00:00]]
   (0.7ms)  commit transaction
...
Completed 200 OK in 139ms (Views: 33.3ms | ActiveRecord: 7.2ms)

And on future requests, since an ingredient with a nil name already exists:

Started GET "/entries/new" for 127.0.0.1 at 2013-04-03 11:20:59 +0200
Processing by EntriesController#new as HTML
  Ingredient Load (0.3ms)  SELECT "ingredients".* FROM "ingredients" LIMIT 1
  Unit Load (0.2ms)  SELECT "units".* FROM "units" 
...
Completed 200 OK in 20ms (Views: 15.9ms | ActiveRecord: 0.4ms)

Bonus information:  you should update your database schema to disallow NULL values on the name.  In my opinion, the database schema should reflect whether a vlaue must be set or defaults to something.

Since I'm pretty much going to need a feature like this in my app soon, I'm interested in your progress smile  Judging from the code, I suggest you add a lot more tests to automate finding out when the form does what you want.  Use `autotest` or setup `guard` to have tests running in the background and shorten feedback loops on code changes.

You already got this:

 # test/functional/entries_controller_test.rb
  test "should create entry" do
    assert_difference('Entry.count') do
      post :create, :entry => @entry.attributes
    end

    assert_redirected_to entry_path(assigns(:entry))
  end

Now add some functional/integration tests which exercise two scenarios from a user's perspective:

  1. Visit the homepage, follow the link to the form or request the form directly. This shouldn't hit the database, i.e. it shouldn't `assert_difference` on Ingredient.count.  Ensure the test database is cleaned before running the test suite, I don't remeber whether that's the default behaviour.

  2. Prepare an ingredient, then request and fill in the form with something plus the ingredient you know exists. Ensure no Ingredient is created.  This test will most likely fail until you're done, that's intended:  you'll know when you're done with this task smile

  3. In the same spirit, add some more controller or request tests in which you call `post :create, ...` to emulate form submission.  Prepare an Ingredient object on test suite setup, request entries#create via post and some working parameters in which you include a way to identify the ingredient.  Be creative:  what do you want a working form (which doesn't exist yet) to tell the controller?  Will `:ingredient => { :name => 'foo' }` do the job, or would you prefer the form submits an ID to distinguish existing and new ingredients?

    And in controller tests, would you rather assert Ingredient.where(...).fine_or_create(...) is called or would you prefer your own interface and grow your app around your own expectations?

    That's the cool part of Behavior- or Test-Driven Development:  you decide how you'd like things to work and then figure out how on earth one could implement that stuff.  The other way 'round, you'd always be limited by what you already know.

If you give that stuff a shot, let me know how it works for you!

Re: How to use dynamic finders?

Thanks for all the pointers and for pointing out that insert statement in the server log, I hadn't noticed that and its clearly not what I'm going for!

My rationale for adding that piece of code to entries#new went more like this: "This probably needs to go in an ingredients controller, but let me see if I can find a place for it in the entries controller.  Looks like entries#new or entries#create might be good candidates.  Since whenever the new entry button is clicked, it goes to the controller action new, and a new entry is initialized each time this happens with @entry = Entry.new, this is probably the place for me to search for or create a new Ingredient."  But now that I restate that and read over your explanation, I realize thats not right at all as it will do this logic whenever a new entry is initialized...  What I want to do is on form submit, or on create but this should work for edits too.  It should search the database for each ingredient that was submitted in the params[:entry][:entry_ingredients_attributes][:ingredient_attributes][:name], create it if it doesn't exist or assign the current one to the entry_ingredient it belongs to.  Sounds simple enough but causing so much headache!

My initial attempt at solving this was a before_save on the model, but I didn't get far with the logic on that one, might go back to that though since I can't think where else to do the check for each ingredient thats being saved.

In regards to the tests, I've never created any tests before, that one you pointed out must have been created with the scaffolding I used when starting the project (never even ran the test once).  I used scaffolding once just to see how it works, I should have deleted the app and started again from scratch since I'm learning.  I do, however, want to learn testing as I think BDD is the best way to create applications because you also then have an easy way to find out if things broke and where they broke.  Was looking into cucumber but it was over my head when I first started out, should give it another look now.  Have a good read on how to get started?

Re: How to use dynamic finders?

Hello

to learn testing you can read

http://everydayrails.com/2012/03/12/tes … intro.html (and the next 3 episodes)
this railscast :http://railscasts.com/episodes/275-how-i-test

Re: How to use dynamic finders?

I agree with you, @gih.  Before you overload your ActiveRecord models with before_save statements, I think the way you learn the most and gain the most in the end is really this:

  1. get confident with TDD/BDD,

  2. test what's there already,

  3. introduce a form handler model which pulls together ActiveRecord codes and which is decoupled from the database during testing.  This should provide a unique and suitable domain specific language via custom methods you yourself invent.  Backed by TDD I feel really comfortable doing stuff like that, while I used to cling to Rails' internals and wanted Rails to solve everything out of the box for me because I didn't dare to stray.

  4. write tests on different levels to state what's missing one after another and make them pass

Maybe this takes an additional week or tws until you feel confident, but the benefits will be huge!

I got my hands on TDD with Rails via extensive web search and reading lots of StackOverflow questions.  I really like the everydayrails.com series @anso pointed out.  It got me started as well.

Check out the following resources:

https://www.relishapp.com/rspec/ -- the 4 divinions make more sense after you know when you search for matchers, expectations, rails-specific features etc.
https://github.com/jnicklas/capybara for driving Request/Feature Specs.  I like this a lot since I didn't benefit from Cucumbers plain text file feature test DSL in my experiments.

I have these pages open during every coding session.  Also you might want to have a look at my bookmarks on these topics:

http://pinboard.in/u:divinedominion/t:rspec
http://pinboard.in/u:divinedominion/t:tdd
http://pinboard.in/u:divinedominion/t:rubyonrails

Also, I've takes lots of notes while I got started since I plan to write an e-book afterwards.  If you think you might benefit from my "dev diary", just tell me.