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?
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 oneInvoice
to manyItem
s.Instead of having a bunch of database records for
InvoiceLineItem
, I would consider a field (e.g. HSTORE or JSONB) that stores theItem
s directly to theInvoice
: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 betweenInvoice
andItem
. And there are lots of great resources about interacting with JSONB columns.It's a bit less intuitive to get "invoices that reference this item", but still very possible (and fast):