[Feature] Using a different column for id in the forms

Hi, There is a requirement that we are exploring and it is to hide the ID of a record in the nested forms for this record and to use something that we call “public_uid”. The reason is security and privacy. It kind of seems difficult to just say to rails “hey, I would like you to use this column as a public id instead of that column”

Is there a reason for this? How would you feel if there is an option to fields_for with id_column_name: “my_public_uid_that_the_world_could_know_about_and_is_not_sequential”. Or probably somewhere on active_record.

This is already in the API. If you set a to_param method on your model, you can choose whatever key you like to identify a record. You have to overload the self.find method as well (although I think there is another method you can use to set this up on that side, too). There is also FriendlyID, which is probably a larger solution than you need.

class Foo < ApplicationRecord
  def to_param
    public_uid
  end

  def self.find(id)
    find_by! public_uid: id
  end
end

Walter

Thanks Walter. You are right that the to_param helps. It generally does this with URLs. FriendlyID also does this with URLs. When i am overriding the to_param method of the user like

class User
  def to_param
    public_uid
  end
end

This will change the url of the user, but if the user is included in a form with fields_for then the generated html will contain the id of the user along with it’s value which in this case is 2 - check out the hidden field for questionnair[child_attributes][id]. Child is the relationship on Questionnaire with a class_name: “User”.

Also looking at FormHelper.rb code it depends strictly on the method being called :id

      def hidden_field(method, options = {})
        @emitted_hidden_id = true if method == :id
        @template.hidden_field(@object_name, method, objectify_options(options))
      end

...
        def fields_for_nested_model(name, object, fields_options, block)
          object = convert_to_model(object)
          emit_hidden_id = object.persisted? && fields_options.fetch(:include_id) {
            options.fetch(:include_id, true)
          }

          @template.fields_for(name, object, fields_options) do |f|
            output = @template.capture(f, &block)
            output.concat f.hidden_field(:id) if output && emit_hidden_id && !f.emitted_hidden_id?
            output
          end
        end

It does not call :to_param and I can see no way to inject and tell fields_for to use another column as the id. So basically even if we hide the ids from the url we can not hide them from the forms. Am I missing something?

That ID will not be in the URL, which is where you were hoping to make the change, right? FriendlyID has a nice fallback in its find method where if it doesn’t get a record with the slug, it tries the primary key (numerical or UUID or whatever was in the base model).

But I would not get too hung up around the literal name id in any code that references an ActiveRecord object. If you use self.primary_key = :wibble in a model, and then refer to that object from another in a association, you don’t have to mention the name of the real primary key in that code. AR takes care of the translation for you.

Walter

Not exactly. I am not hoping to remove the id from the url, this is easily done with to_param. I am hoping to remove the id from the form generated with f.fields_for (which does not take into account the to_param method and does not call it. It calls the :id method of the record)

But I would not get too hung up around the literal name id in any code that references an ActiveRecord object. If you use self.primary_key = :wibble in a model, and then refer to that object from another in a association, you don’t have to mention the name of the real primary key in that code. AR takes care of the translation for you.

That could do. Will try it out.

Easiest way is to use signed_id and find_signed. This will completely remove the ID from the url AND the form.

<%= form @user, path: users_path(@user.signed_id) do |form| %>
<% end %>
class UsersController
  def create
    @user = User.find_signed(params[:id])
  end
end

A signed ID looks like this:

eyJfcmFpbHMiOnsibWVzc2FnZSI6Ik16ZzBOVFkzT1E9PSIsImV4cCI6bnVsbCwicHVyIjoidXNlciJ9fQ==--dee711538e64cc9faba203a4c0f2164a90800954819796e30b40dafb6a763812

We use this when we want to provide secure access to visitors who might not be logged in.