1

I've recently picked up maintenance of a couple of Rails 5.2 apps with a PostgreSQL back end. I'm new to Rails, but I've got a fair bit of experience on the various Microsoft platforms.

I'm trying to add API calls to an existing model. When I attempt to create a new instance, I am not getting the database-generated ID back:

POST /invoices
{ "amount": 12.34 }

Invoice Create (4.0ms)  
  INSERT INTO "invoices" ("amount", "created_at", "updated_at") 
  VALUES ($1, $2, $3)  
  [["amount", 12.34], ["created_at", "..."], ["updated_at", "..."]]

201 Created
{ "id": null, "amount": 12.34 }

Checking the database, the new row is present, with a unique ID.

A different model in the same app generates different SQL and works as expected:

POST /customer
{ "name": "ACME" }

Customer Create (1.4ms)  
  INSERT INTO "customers" ("name", "created_at", "updated_at") 
  VALUES ($1, $2, $3) 
  ** RETURNING "id" **  
  [["name", "ACME"], ["created_at", "..."], ["updated_at", "..."]]

201 Created
{ "id": 111, "name": "ACME" }

I can't see any differences in the two models that explain this behavior. I've checked everything I can think of:

  • routes (via :resources)
  • controller
    • before/after filters
    • strong parameters
    • code in create
  • model
    • neither contains any code
  • schema
    • column definitions are comparable in schema.rb and information_schema.columns

Here's the model and controller for the misbehaving type:

class Invoice < ActiveRecord::Base
end

class InvoiceController < ApplicationController
  def create
    invoice = Invoice.new(invoice_params)
    if invoice.save
      # invoice.id.nil? => true
      render json: invoice, status: :created
    end
  end

  def invoice_params
    params.permit(:amount)
  end
end

# schema.rb
create_table "invoices", id: false, force: :cascade do |t|
  t.serial "id", null: false
  t.float "amount"
  t.datetime "created_at"
  t.datetime "updated_at"
end

And the one that works as expected:

class Customer < ActiveRecord::Base
end

class CustomerController < ApplicationController
  def create
    customer = Customer.new(customer_params)
    if customer.save
      # customer.id.nil? => false
      render json: customer, status: :created
    end
  end

  def customer_params
    params.permit(:name)
  end
end

# schema.rb
create_table "customers", id: :serial, force: :cascade do |t|
  t.string "name"
  t.datetime "created_at"
  t.datetime "updated_at"
end

Replacing new/save with create or create! doesn't change the behavior, so I'm convinced that the problem is somewhere in the model definition or metadata.

Creating the models from rails console has the same result as shown below:

irb(main):001:0> Invoice.create(amount:12.34)
   (0.8ms)  BEGIN
  Invoice Create (1.1ms)  INSERT INTO "invoices" ("amount", "created_at", "updated_at") VALUES ($1, $2, $3)  [["amount", 12.34], ["created_at", "2021-11-19 09:10:33.490117"], ["updated_at", "2021-11-19 09:10:33.490117"]]
   (5.8ms)  COMMIT
=> #<Invoice id: nil, amount: 12.34, created_at: "2021-11-19 09:10:33", updated_at: "2021-11-19 09:10:33">

irb(main):002:0> Customer.create(name: "ACME")
   (0.9ms)  BEGIN
  Customer Create (1.5ms)  INSERT INTO "customers" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["name", "ACME"], ["created_at", "2021-11-19 09:12:50.492927"], ["updated_at", "2021-11-19 09:12:50.492927"]]
   (13.3ms)  COMMIT
=> #<Customer id: 24, name: "ACME", created_at: "2021-11-19 09:12:50", updated_at: "2021-11-19 09:12:50">

Can anyone point me in the right direction?

  • You need to post the model code, there must be code in them even if it is just a class declaration, also the controller actions please – jamesc Nov 19 '21 at 00:28
  • thanks @jamesc, i've updated the post. i should have stated the models contain just the class declaration :) – Gavin Morris Nov 19 '21 at 00:41
  • What JSON templates have you got? In the view folders – jamesc Nov 19 '21 at 00:45
  • If you just want the raw jsin from the model then your action can return object to_json but you are probably formatting the response in a template, this 8s normally returned in a respond_to block – jamesc Nov 19 '21 at 00:50
  • Right now, there's nothing at all in the view folders for these models. Originally there was a show.json.builder that didn't include the id, but adding it didn't solve the problem, so I changed to `render json:` as shown above. – Gavin Morris Nov 19 '21 at 00:51
  • Similarly, `create` originally had the usual respond_to html/json block, but I've stripped that out for now while investigating this issue. Code as shown above is my current minimal repro. – Gavin Morris Nov 19 '21 at 00:59
  • Interesting that you are not using instance variables which is important if you are trying to render a json template. I think for th8s simplistic problem you should just render invoice.to_json – jamesc Nov 19 '21 at 01:12
  • But you are definitely trying to render a json template and your original problem was possibly related to not using instance variables and also, when unexpected behaviour happens it 8s often advisable to restart your server – jamesc Nov 19 '21 at 01:14
  • I've updated the question again to clarify that the problem is to do with the state of the model after the call to `save` or `create`, in that `id` is nil. – Gavin Morris Nov 19 '21 at 02:07

2 Answers2

1

The difference is that you explicitly declared "id" as a column, and disabled the default primary key "id" declaration / handling.

If you change to:

create_table "invoices", id: :serial, force: :cascade do |t|
  t.float "amount"
  # These are normally created automatically so not sure why they are here
  # t.datetime "created_at"
  # t.datetime "updated_at"
end

it should work.

This answer may also help: https://stackoverflow.com/a/54694863/224837

MZB
  • 2,071
  • 3
  • 21
  • 38
-1

This is caused by the missing primary key on the tables in question. It looks like these tables might have been created manually at some point early in the project, and have only been written to by external SQL scripts until now.