New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Postgres default function values do not get loaded on create #34237

Open
davegson opened this Issue Oct 17, 2018 · 5 comments

Comments

Projects
None yet
5 participants
@davegson

davegson commented Oct 17, 2018

Steps to reproduce

1. Create a model

class Order < ApplicationRecord
end

2. Create a migration with a DB function as default.

class CreateOrders < ActiveRecord::Migration[5.2]
  def change
    create_table :orders do |t|
      t.string :uuid, default: -> { "gen_random_uuid()" }
      t.references :user
      t.references :product
      t.text :comment

      t.timestamps
    end
  end
end

rails db:migrate

3. Create a new order

rails console

order = Order.create(user_id: 1, product_id: 1)

Expected behavior

I expect create to set uuid (by the postgres default) and for it to set the new uuid value into the order AR instance, as it does with the id.

> order
# =>
# +----+--------------------------------------+---------+------------+---------+----------------+----------------+
# | id | uuid                                 | user_id | product_id | comment | created_at     | updated_at     |
# +----+--------------------------------------+---------+------------+---------+----------------+----------------+
# | 1  | bec94fe0-f7c3-4ede-8b25-c4bce702ec00 | 1       | 1          |         | 2018-10-17 ... | 2018-10-17 ... |
# +----+--------------------------------------+---------+------------+---------+----------------+----------------+

Actual behavior

The uuid does get set in the db, but it is not returned into the order instance.

> order
# =>
# +----+------+---------+------------+---------+----------------+----------------+
# | id | uuid | user_id | product_id | comment | created_at     | updated_at     |
# +----+------+---------+------------+---------+----------------+----------------+
# | 1  |      | 1       | 1          |         | 2018-10-17 ... | 2018-10-17 ... |
# +----+------+---------+------------+---------+----------------+----------------+

Current workaround

Calling order.reload does then retrieve the values set by postgres. Other workarounds are described in this issue

> order
# =>
# +----+--------------------------------------+---------+------------+---------+----------------+----------------+
# | id | uuid                                 | user_id | product_id | comment | created_at     | updated_at     |
# +----+--------------------------------------+---------+------------+---------+----------------+----------------+
# | 1  | bec94fe0-f7c3-4ede-8b25-c4bce702ec00 | 1       | 1          |         | 2018-10-17 ... | 2018-10-17 ... |
# +----+--------------------------------------+---------+------------+---------+----------------+----------------+

Background

This issue already describes this bug, but the issue was initially a request to support functional defaults, which has since been implemented.

This quote from the discussion summarizes it pretty well:

@kenaniah Sure, but it makes sense why the full row shouldn't be returned by default: there could be too much data, and ActiveRecord already knows both default and non-default values. (Or at least it thinks so.) But if the default value is generated by a function in PG then ActiveRecord doesn't know. Turns out that is not enough reason to make RETURNING * the default, and I'm ok with that. I think what we need is another parameter to tell ActiveRecord, that for a particular model, it should use RETURNING * instead.

This blog post describes the issue wonderfully.

Next steps

So a solution could be an option to tell AR to use RETURNING * in some instances. But I myself am not familiar with the ActiveRecord internals so I feel this should be further discussed.

System configuration

Rails version: 2.4.1

Ruby version: 5.2.1

Postgres version: 9.6.3

@davegson davegson changed the title from Postgres default function does not get loaded on create to Postgres default function values do not get loaded on create Oct 17, 2018

@jrochkind

This comment has been minimized.

Contributor

jrochkind commented Oct 17, 2018

Rather than RETURNING *, perhaps ActiveRecord could know which columns have proc default values (which I think is what means "direct to DB that can call functions"), and just RETURNING those in addition to id?

But either way would be an improvement.

@yawboakye

This comment has been minimized.

Contributor

yawboakye commented Oct 20, 2018

Currently the solution is to immediately reload the model (and that's what I do). That's not much of a penalty (an extra DB inquiry). There's many other things that affect what value eventually ends up in the column (e.g. triggers) which makes me lean more towards a reload after_save. In that case a directive to append a RETURNING * clause to the generated SQL for INSERT and UPDATE for a model and to build the object out of those values would make more sense.

@jrochkind

This comment has been minimized.

Contributor

jrochkind commented Oct 20, 2018

Oh good point, rather than AR try to automatically discover what to put in RETURNING a directive makes a lot of sense, and is probably pretty straightforward implementation. Directive could be either on the model (for all saves), or an argument to save. Probably the first, or both.

My current work-around is by doing something a little bit different than a reload. I just override the relevant attribute method to do a lazy pluck if the thing is persisted? and the value is nil.

Really it ought to check not just persisted? but a @has_just_been_created set in an after_create too, to only try to do the fetch if the in-memory object was just inserted, not fetched from the db. And as you can see, need to do some odd (and possibly private API?) things to make sure the fetch of the db-provided value doesn't make AR think the model is "dirty". And this technique would start to break down if I had more than one column set from the db (or require an even more complex implementation to make sure there was only one fetch for all of those attributes. (Or just go back to the non-lazy reload). So this is a pretty kludgey and error-prone workaround. It would be better if AR could just handle this with built-in API one way or another.

  # Due to rails bug, we don't immediately have the database-provided value after create. :(
  # If we ask for it and it's empty, go to the db to get it
  # https://github.com/rails/rails/issues/21627
  def friendlier_id(*_)
    in_memory = super

    if !in_memory && persisted? && !@friendlier_id_retrieved
      in_memory = self.class.where(id: id).limit(1).pluck(:friendlier_id).first
      write_attribute(:friendlier_id, in_memory)
      clear_attribute_change(:friendlier_id)
      # just to avoid doing it multiple times if it's still unset in db for some reason
      @friendlier_id_retrieved = true
    end

    in_memory
  end
@tbuehlmann

This comment has been minimized.

tbuehlmann commented Nov 6, 2018

I tackled that exact problem in a POC over here. It wasn't that simple as I had to override quite some AR core code. So it's not a very great implementation and will probably break as soon as internals change.

@davegson

This comment has been minimized.

davegson commented Nov 6, 2018

If this will be implemented, I it will definitely override some AR core code ;) so it's a great reference!

Still, for this I assume we need somebody very familiar with the AR internals

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment