Welcome to Working With Rails

 

Discussion Forums

Discuss all things Ruby on Rails with perhaps the web's most vibrant group of Ruby on Rails enthusiasts.
How to code an inverted relationship
9 Posts
How to code an inverted relationship

I have the following model, Entity, with the attributes: id, entity_name, entity_legal_name and entity_legal_form. Entity has a unique index on (lower(name)).

I have two dependent models, Clients and Vendors, each with attributes (credit_policy, payment_terms) specific to either role. An entity may be a client, a vendor, both or neither. If it exists then a client and a vendor must be an entity as well.

What I wish to do is to create clients (and vendors) directly and create the related entity only if it does not already exist, or return the values of entity if it is already on file (lookup by name).

My problem is that I have no clue on how do do this in rails. I have looked at various screencasts/railscasts on complex forms and various bits and piece but I cannot seem to grasp how this is done.

In client.rb I have:

class Client < ActiveRecord::Base

belongs_to :entity

validates_presence_of :entity_id validates_presence_of :client_credit_policy validates_presence_of :client_credit_terms

end

In entity.rb I have:

class Entity < ActiveRecord::Base

has_one :client

validates_presence_of :entity_name validates_presence_of :entity_legal_name validates_presence_of :entity_legal_form

end

In the clients_controller.rb I have:

.... # GET /clients/new # GET /clients/new.xml def new

@client = Client.new
# TODO JBB20080311 This should really depend upom
# a lookup on entities via the name.  If the entity
# is on file then use it.
@entity = Entity.new

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

end

# GET /clients/1/edit def edit

@client = Client.find(params[:id])
@entity = Client.entity

end ...

and in the view I have:

h1>New client

<b>Entity Name</b><br />

<b>Entity Legal Style</b><br />

<b>Client status</b><br />

<b>Client credit policy</b><br />

<b>Client credit terms</b><br />

<b>Effective from</b><br />

<b>Superseded after</b><br />

The code does not work, but it does not blow up either.

I do not know how to get rails to do the following when adding a client:

  1. If entity.name.downcase is on file then return that entity.id and set client_entity_id to it and then add the client if a client record does not already exist.

Else

  1. If entity.name.downcase is not on file then create the entity record and and the client record.

This is simply a prototype to get a handle on the coding techniques. In the production case one would probably wish to display an entity input partial to pickup any other fields, add that record and then return to the client. But for now I just want to see how this is wired together in rails.

If anyone reading this is interested in helping me for a fee then I am considering hiring a tele-mentor to get me up to speed on this. I envisage generating these sorts of requests and having code snippets returned with an explanation within 24 hours or so. If anyone is interested please let me know.

in client.rb use a before_save like this.

before_save: create_entity

def create_entity @entity = Entity.find_or_create_by_client_id(self.id) self.entity_id = @entity.id end

Any other fields you need to create or the entity you would pass to the find_or_create method. Hope this helps.

Thank you very much. I will try this and post back with the results.

I have been working a bit on this and have made some progress. What I decided to do was to bind the relevant entity attributes to the client model, since an entity can only have a single client role. So I did this in client.rb:

00027 # virtual attributes to handle entity attributes bound to client 00028 def client_name 00029 self.client_name = @entity.entity_name.titlecase 00030 end 00031 00032 def client_name=(name) 00033 @entity.entity_name = name.keycase 00034 end 00035
00036 def client_legal_name 00037 self.client_legal_name = @entity.entity_legal_name 00038 end 00039
00040 def client_legal_name=(legal_name) 00041 @entity.entity_legal_name = legal_name 00042 end 00043
00044 def client_legal_form 00045 self.client_legal_form = @entity.entity_legal_form 00040 end

# Legal form is a value attribute and is upshifted def client_name=(legal_form)

@entity.entity_legal_form = legal_form.upcase

end

Note that titlecase and keycase are application helper methods.

I also added this code to clients.rb as suggested.

validates_associated

before_save :find_or_create_entity

def find_or_create_entity

# keycase is defined in application helpers.
entity_name = keycase(self.client_name)
# This is a reasonable default
entity_legal_name = client_legal_name
@entity = Entity.find_or_create_by_entity_name(
  :entity_name => client_name, 
  :entity_legal_name => client_legal_name,
  # Perhaps this sould be returned from a virtual
  # attribute (self.legal_form)
  :entity_legal_form => client_legal_form
)
@self.entity_id = @entity.id

end

In clients_controller.rb I did this:

def new

# handle the entity entirely in the client model
@client = Client.new

#@entity = Entity.new

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

end

And in views/clients/new.html.erb I did this:

New client

<b>Client Name</b><br />

<b>Client Legal Name</b><br />

<b>Client Legal Form</b><br />

When I try and add / create a new entity/client pair I now get this:

NoMethodError in ClientsController#create

You have a nil object when you didn't expect it! The error occurred while evaluating nil.entity_name

RAILS_ROOT: /home/byrnejb/Software/Development/Projects/invert Application Trace | Framework Trace | Full Trace

app/models/client.rb:29:in client_name' app/models/client.rb:14:infind_or_create_entity' app/controllers/clients_controller.rb:52:in create' app/controllers/clients_controller.rb:51:increate'

/usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/callbacks.rb:307:in send' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/callbacks.rb:307:incallback' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/callbacks.rb:304:in each' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/callbacks.rb:304:incallback' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/callbacks.rb:212:in create_or_update' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/base.rb:1972:insave_without_validation' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/validations.rb:934:in save_without_transactions' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/transactions.rb:108:insave' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/connection_adapters/abstract/database_statements.rb:66:in transaction' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/transactions.rb:80:intransaction' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/transactions.rb:100:in transaction' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/transactions.rb:108:insave' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/transactions.rb:120:in rollback_active_record_state!' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/transactions.rb:108:insave' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/mime_responds.rb:106:in call' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/mime_responds.rb:106:inrespond_to' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/base.rb:1158:in send' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/base.rb:1158:inperform_action_without_filters' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/filters.rb:697:in call_filters' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/filters.rb:689:inperform_action_without_benchmark' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/benchmarking.rb:68:in perform_action_without_rescue' /usr/lib/ruby/1.8/benchmark.rb:293:inmeasure' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/benchmarking.rb:68:in perform_action_without_rescue' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/rescue.rb:199:inperform_action_without_caching' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/caching.rb:678:in perform_action' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/connection_adapters/abstract/query_cache.rb:33:incache' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/query_cache.rb:8:in cache' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/caching.rb:677:inperform_action' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/base.rb:524:in send' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/base.rb:524:inprocess_without_filters' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/filters.rb:685:in process_without_session_management_support' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/session_management.rb:123:inprocess' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/base.rb:388:in process' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/dispatcher.rb:171:inhandle_request' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/dispatcher.rb:115:in dispatch' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/dispatcher.rb:126:indispatch_cgi' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/dispatcher.rb:9:in dispatch' /usr/lib/ruby/gems/1.8/gems/rails-2.0.2/lib/webrick_server.rb:112:inhandle_dispatch' /usr/lib/ruby/gems/1.8/gems/rails-2.0.2/lib/webrick_server.rb:78:in service' /usr/lib/ruby/1.8/webrick/httpserver.rb:104:inservice' /usr/lib/ruby/1.8/webrick/httpserver.rb:65:in run' /usr/lib/ruby/1.8/webrick/server.rb:173:instart_thread' /usr/lib/ruby/1.8/webrick/server.rb:162:in start' /usr/lib/ruby/1.8/webrick/server.rb:162:instart_thread' /usr/lib/ruby/1.8/webrick/server.rb:95:in start' /usr/lib/ruby/1.8/webrick/server.rb:92:ineach' /usr/lib/ruby/1.8/webrick/server.rb:92:in start' /usr/lib/ruby/1.8/webrick/server.rb:23:instart' /usr/lib/ruby/1.8/webrick/server.rb:82:in start' /usr/lib/ruby/gems/1.8/gems/rails-2.0.2/lib/webrick_server.rb:62:indispatch' /usr/lib/ruby/gems/1.8/gems/rails-2.0.2/lib/commands/servers/webrick.rb:66 /usr/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in gem_original_require' /usr/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:inrequire' /usr/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:496:in require' /usr/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:342:innew_constants_in' /usr/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:496:in require' /usr/lib/ruby/gems/1.8/gems/rails-2.0.2/lib/commands/server.rb:39 /usr/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:ingem_original_require' /usr/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `require' script/server:3

app/models/client.rb:29:in client_name' app/models/client.rb:14:infind_or_create_entity' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/callbacks.rb:307:in send' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/callbacks.rb:307:incallback' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/callbacks.rb:304:in each' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/callbacks.rb:304:incallback' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/callbacks.rb:212:in create_or_update' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/base.rb:1972:insave_without_validation' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/validations.rb:934:in save_without_transactions' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/transactions.rb:108:insave' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/connection_adapters/abstract/database_statements.rb:66:in transaction' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/transactions.rb:80:intransaction' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/transactions.rb:100:in transaction' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/transactions.rb:108:insave' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/transactions.rb:120:in rollback_active_record_state!' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/transactions.rb:108:insave' app/controllers/clients_controller.rb:52:in create' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/mime_responds.rb:106:incall' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/mime_responds.rb:106:in respond_to' app/controllers/clients_controller.rb:51:increate' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/base.rb:1158:in send' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/base.rb:1158:inperform_action_without_filters' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/filters.rb:697:in call_filters' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/filters.rb:689:inperform_action_without_benchmark' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/benchmarking.rb:68:in perform_action_without_rescue' /usr/lib/ruby/1.8/benchmark.rb:293:inmeasure' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/benchmarking.rb:68:in perform_action_without_rescue' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/rescue.rb:199:inperform_action_without_caching' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/caching.rb:678:in perform_action' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/connection_adapters/abstract/query_cache.rb:33:incache' /usr/lib/ruby/gems/1.8/gems/activerecord-2.0.2/lib/active_record/query_cache.rb:8:in cache' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/caching.rb:677:inperform_action' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/base.rb:524:in send' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/base.rb:524:inprocess_without_filters' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/filters.rb:685:in process_without_session_management_support' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/session_management.rb:123:inprocess' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/base.rb:388:in process' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/dispatcher.rb:171:inhandle_request' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/dispatcher.rb:115:in dispatch' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/dispatcher.rb:126:indispatch_cgi' /usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/dispatcher.rb:9:in dispatch' /usr/lib/ruby/gems/1.8/gems/rails-2.0.2/lib/webrick_server.rb:112:inhandle_dispatch' /usr/lib/ruby/gems/1.8/gems/rails-2.0.2/lib/webrick_server.rb:78:in service' /usr/lib/ruby/1.8/webrick/httpserver.rb:104:inservice' /usr/lib/ruby/1.8/webrick/httpserver.rb:65:in run' /usr/lib/ruby/1.8/webrick/server.rb:173:instart_thread' /usr/lib/ruby/1.8/webrick/server.rb:162:in start' /usr/lib/ruby/1.8/webrick/server.rb:162:instart_thread' /usr/lib/ruby/1.8/webrick/server.rb:95:in start' /usr/lib/ruby/1.8/webrick/server.rb:92:ineach' /usr/lib/ruby/1.8/webrick/server.rb:92:in start' /usr/lib/ruby/1.8/webrick/server.rb:23:instart' /usr/lib/ruby/1.8/webrick/server.rb:82:in start' /usr/lib/ruby/gems/1.8/gems/rails-2.0.2/lib/webrick_server.rb:62:indispatch' /usr/lib/ruby/gems/1.8/gems/rails-2.0.2/lib/commands/servers/webrick.rb:66 /usr/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in gem_original_require' /usr/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:inrequire' /usr/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:496:in require' /usr/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:342:innew_constants_in' /usr/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:496:in require' /usr/lib/ruby/gems/1.8/gems/rails-2.0.2/lib/commands/server.rb:39 /usr/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:ingem_original_require' /usr/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `require' script/server:3

Request

Parameters:

{"commit"=>"Create", "entity"=>{"entity_legal_name"=>"Something Else", "id"=>"", "entity_name"=>"A new Entity"}, "authenticity_token"=>"b14082d9c57cfea6f151450cf9fd5334d91cf366", "client"=>{"superseded_after(2i)"=>"3", "superseded_after(3i)"=>"12", "client_credit_policy"=>"CASH", "effective_from(1i)"=>"2008", "effective_from(2i)"=>"3", "client_status"=>"HOLD", "effective_from(3i)"=>"12", "client_credit_terms"=>"0", "superseded_after(1i)"=>"2008"}}

Which looks a lot better than what I was getting before even if it still fails. The main thing is that the parameters look to be very close to what I expect. At least I cannot find anything wrong with them. But I am getting an error that says that I have a nil.entity_name which points to line 29.

Any suggestions as to what I am neglecting to do with respect to setting up the entity instance?

Well, I am down to an SQL insert error, which way beyond where I was. The code now looks like:

client.rb

class Client < ActiveRecord::Base

belongs_to :entity

validates_associated

end

clients_controller.rb

... # POST /clients # POST /clients.xml def create

@entity = Entity.new(params[:entity])
@client = Client.new(params[:client])

respond_to do |format|
  if @entity.save && @client.save
    flash[:notice] = 'Client was successfully created.'
    format.html { redirect_to(@client) }
    format.xml  { render :xml => @client, :status => :created, :location => @client }
  else
    format.html { render :action => "new" }
    format.xml  { render :xml => @client.errors, :status => :unprocessable_entity }
  end
end

end ...

and views/clients/new.html.erb

<p>
  <b>Client Name</b><br />

</p>

<p>  
  <b>Client Legal Name</b><br />

</p>

<p>  
  <b>Client Legal Form</b><br />

</p>

<b>Client status</b><br />


This code properly validates the fields in entity and clients but when I pass the edits and go to update the database this is what I get:

Parameters: {"commit"=>"Create", "entity"=>{"entity_legal_name"=>"The NEW client's fuLL name", "entity_name"=>"A NEW client", "entity_legal_form"=>"PERS"}, "client"=>{"superseded_after(2i)"=>"3", "superseded_after(3i)"=>"13", "client_credit_policy"=>"CASH", "effective_from(1i)"=>"2008", "effective_from(2i)"=>"3", "client_status"=>"HOLD", "effective_from(3i)"=>"13", "client_credit_terms"=>"0", "superseded_after(1i)"=>"2008"}, "authenticity_token"=>"c453fa992f3d1755c689be9187537c6ccc5f8c89", "action"=>"create", "controller"=>"clients"} Entity Create (0.001668) INSERT INTO entities ("updated_at", "entity_legal_name", "entity_name", "created_at", "entity_legal_form") VALUES('2008-03-13 11:37:34', 'The NEW client''s fuLL name', 'A NEW client', '2008-03-13 11:37:34', 'PERS') Client Create (0.000000) SQLite3::SQLException: SQL logic error or missing database: INSERT INTO clients ("entity_id", "updated_at", "client_credit_policy", "client_status", "effective_from", "superseded_after", "client_credit_terms", "created_at") VALUES(NULL, '2008-03-13 11:37:34', 'CASH', 'HOLD', '2008-03-13', '2008-03-13', 0, '2008-03-13 11:37:34')

Now, this looks to me as if the entity_id in clients is being set to null. Since this is the foreign key constraint on clients then it seems reasonable that this would throw an error but why is not Rails setting this value? What do I have to do to get this to work?

The answer is found in the ActiveRecord::Associations::ClassMethods API

in the controller do:

# GET /clients/new # GET /clients/new.xml def new

@entity = Entity.new
@client = @entity.build_client

...

# GET /clients/1/edit def edit

@entity = Entity.find(params[:id])
@client = @entity.client

end

# POST /clients # POST /clients.xml def create

@entity = Entity.new(params[:entity])
@client = @entity.build_client(params[:client])

and it works.

To me this seems like a great use of STI. Include a type column and generate your entity model first, then subclass the specific methods and internals to Client and Vendor. You would use it like:

client = Client.new(params[:client]) vendor = Vendor.new(params[:client])

Now client and vendor are both Entities and their specific type as well.

Next generate your controllers for Client and Vendor.

Person
Sr. Ruby & Rails Consultant For Hire
Sign In To Rate Post

To me this seems like a great use of STI.

Granted and, as it turns out, the database backend that will be used in production, PostgreSQL, actually supports table inheritance.

However, that is not the option that I selected for this specific design. The idea I have in mind is that entity models a real world thing, be it person, partnership, or corporation. Whereas client, vendor, person, etc, model the roles that an entity can possess. A specific role reflects through entity to obtain the various dependent attributes that an entity has, such as locations, comm. channels, contacts, etc. independent of the role. This issue is significant in this project because there are many entities involved with transactions that are neither clients nor vendors.

My problem with using roles for this type of problem is that you then map controller/actions to rights in a role, when it is easier to define a model with the role and that model can have methods independent of the other roles.

The User table holds all the data, then hides it through the accessor protection then the model that needs the attributes exposes them.

Good luck however you implement it.

Person
Sr. Ruby & Rails Consultant For Hire
Sign In To Rate Post
9 Posts
Login to add your message