Home > Enterprise >  Rails Has Many Validate Unique Attribute With Accepts Nested Attributes For
Rails Has Many Validate Unique Attribute With Accepts Nested Attributes For

Time:11-16

I have Invoices with many Invoice Line Items. Invoice line items point to a specific item. When creating or updating an Invoice, I'd like to validate that there is not more than 1 invoice line item with the same Item (Item ID). I am using accepts nested attributes and nested forms.

I know about validates_uniqueness_of item_id: {scope: invoice_id}

However, I cannot for the life of me get it to work properly. Here is my code:

Invoice Line Item

belongs_to :item

validates_uniqueness_of :item_id, scope: :invoice_id

Invoice

has_many :invoice_line_items, dependent: :destroy
accepts_nested_attributes_for :invoice_line_items, allow_destroy: true

Invoice Controller

  // strong params
  params.require(:invoice).permit(
    :id,
    :description, 
    :company_id, 
    invoice_line_items_attributes: [
      :id,
      :invoice_id,
      :item_id,
      :quantity,
      :_destroy
    ]
  )
  // ...
  // create action
  def create
    @invoice = Invoice.new(invoice_params)

    respond_to do |format|
      if @invoice.save
         
        format.html { redirect_to @invoice }
      else
        format.html { render action: 'new' }
      end
    end
  end

The controller code is pretty standard (what rails scaffold creates).

UPDATE - NOTE that after more diagnosing, I find that on create it always lets me create multiple line items with the same item when first creating an invoice and when editing an invoice without modifying the line items, but NOT when editing an invoice and trying to add another line item with the same item or modifying an attribute of one of the line items. It seems to be something I'm not understanding with how rails handles nested validations.

UPDATE 2 If I add validates_associated :invoice_line_items, it only resolves the problem when editing an already created invoice without modifying attributes. It seems to force validation check regardless of what was modified. It presents an issues when using _destroy, however.

UPDATE 3 Added controller code.

Question - how can I validate an attribute on a models has many records using nested form and accepts nested attributes?

CodePudding user response:

I know this isn't directly answering your qestion, but I would do things a bit differently.

The only reason InvoiceLineItem exists is to associate one Invoice to many Items.

Instead of having a bunch of database records for InvoiceLineItem, I would consider a field (e.g. HSTORE or JSONB) that stores the Items directly to the Invoice:

> @invoice.item_hash
> { #item1: #quantity1, #item2: #quantity2, #item3: #quantity3, ... }

Using the :item_id as a key in the hash will prevent duplicate values by default.

A simple implementation is to use ActiveRecord::Store which involves using a text field and letting Rails handle serialization of the data.

Rails also supports JSON and JSONB and Hstore data types in Postgresql and JSON in MySQL 5.7

Lookups will be faster as you don't need to traverse through InvoiceLineItem to get between Invoice and Item. And there are lots of great resources about interacting with JSONB columns.

# invoice.rb
...
def items
  Item.find( item_hash.keys)
end

It's a bit less intuitive to get "invoices that reference this item", but still very possible (and fast):

# item.rb
...
# using a Postgres JSON query operator:
# jsonb ? text → boolean (Does the text string exist as a top-level key or array element within the JSON value?)
# https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSONB-OP-TABLE
def invoices
  Invoice.where('item_hash ? :key', key: id)
end

CodePudding user response:

After reading this through a few times I think see it as:

An invoice has many invoice_line_items

An invoice has many items through invoice_line_items

Item_id needs to be unique to every invoice, thus no repeating of items in the line_item list.

Items are a catalogue of things that can show up in multiple invoices. i.e. items are things like widget_one and widget_two, and an invoice can only contain one line with widget one, but many invoices could contain this same item. If this is not true and an item will only ever show up in one invoice, let me know and I will change my code.

So I think your validation should not be in Items, as items know nothing about invoices. You want to make sure your join table has no entries where a given invoice_id has duplicate item_id entries.

item.rb:

has_many :invoice_line_items
has_many :invoices, through: :invoice_line_items

invoice.rb:

has_many :invoice_line_items
has_many :items, through: :invoice_line_items

invoice_line_item.rb:

belongs_to :item
belongs_to :invoice
validates_uniqueness_of :item_id, :scope => :invoice_id

This SHOULD give you what you need. When you create a new invoice and save it, Rails should try to save each of the line items and the join table. When it hits the duplicate item_id it should fail and throw an error.

If you are going to have unique constraints in your code I would alway back it up with a constraint in your database so that if you do something that makes an end run around this code it will still fail. So a migration should be added to do something like:

add_uniq_constraint_to_invoice_line_items.rb:

def change
  add_index :invoice_line_items, [:invoice_id, :item_id], unique: true    
end

This index will prevent creation of a record with those two columns the same.

  • Related