AirWick219
AirWick219

Reputation: 944

Clean way to return ActiveRecord object with associations

I want to return all the Thing model objects with just the associations without the asscoiation_id, is there a better way to do this without include and except?

# Thing.rb

belongs_to :object_a
belongs_to :object_b

# create_thing.rb

def change
  create_table :things, id: false do |t|
    t.string :id, limit: 36, primary_key: true
    t.string :object_a_id, foreign_key: true
    t.string :object_b_id, foreign_key: true

    t.timestamps
  end
end
# things_controller.rb

render json: Thing.all, include: [:object_a, :object_b]

output => {
  id: ....
  object_a_id: 'object_a_id',
  object_b_id: 'object_b_id',
  object_a: {
    id: object_a_id
    ...
  },
  object_b: {
    id: object_b_id
    ...
  }

I know I can do this to get what I want but I wondered if there is a DRY way to do this without all the include and except.

render json: Thing.all, include: [:object_a, :object_b], except: [:object_a_id, :object_b_id]

output => {
  id: ....
  object_a: {
    id: object_a_id
    ...
  },
  object_b: {
    id: object_b_id
    ...
  }

Upvotes: 1

Views: 837

Answers (1)

bwalshy
bwalshy

Reputation: 1135

Solution

A DRY approach is inside your models, you can define a attributes method and have it return the shape of the object that you want the render function to use.

# thing.rb

def attributes
  # Return the shape of the object
  # You can use symbols if you like instead of string keys
  {
    'id' => id,                      # id of the current thing
    'other_fields' => other_fields,  # add other fields you want in the serialized result   
    'object_a' => object_a,          # explicitly add the associations
    'object_b' => object_b
  }
end

The associations object_a and object_b should get serialized as normal. You can repeat the same approach for them by adding an attributes method in their respective classes if you want to limit/customize their serialized result.

So when render json: is called on a single, or a collection of, thing model(s), the shape of the json objects returned will be as defined in the method above.

Note:

One caveat is your key names in the hash being returned in attributes has to match the name of the method (or association name). I'm not too sure why. But the workaround that I've used when needing to add a key with a different name than its corresponding column is to make a method in the model of the key name I want to use.

For example, say your Thing model has a column name, but in your json result you want the key name that corresponds to that column to be called name_of_thing. You'd do the following:

def name_of_thing
  name
end

def attributes
  {
    'name_of_thing' => name_of_thing,
    # other fields follow
    # ...
  }
end

Conditional Logic

Conditions dependent on fields/associations in the model

The attributes method can support conditional based on fields in the model.

# thing.rb

def attributes
  result = {}

  result['id'] = id
  # add other fields

  # For example, if association to object_a exists
  if object_a
    result.merge!({
      'object_a' => object_a
    })
  end

  # return result
  result
end

Conditions dependent on input from outside the model

If you want to make your method render different fields in different places, one thing you can do is override the as_json method, which can work better for these cases because this method accepts options in a parameter.

# thing.rb

def as_json(options = {})
  result = {}

  result['id'] = id
  # add other fields to result

  if(options[:show_only_ids])
    result.merge!({
      'object_a_id' => object_a_id,
      'object_b_id' => object_b_id
    })
  else
    result.merge!({
      'object_a' => object_a,
      'object_b' => object_b
    })
  end

  # return result
  result
end

Then you need to modify your controller (or wherever you're calling the serialization of the Thing model) to pass in the appropriate option when necessary.

# thing_controller.rb

render json: Thing.all.as_json({ show_only_ids: true })

When rendering, you don't always have to explicitly specify the as_json. The render function will call it anyway by default. You only need to explicitly make that call when you want to pass in options.

Upvotes: 1

Related Questions