fvosberg
fvosberg

Reputation: 697

An ordered has_many association in rails 4

I think it's not an uncommon requirement, but I can't find a proper solution.

I have a recipe model with a n:1 Relation to instructions.

class Recipe < ActiveRecord::Base
    has_many :instructions, autosave: true, class_name: 'RecipeInstruction'
end

No I want to order the instructions in a recipe by hand. So my first approach was to add a position attribute to instructions and add the following.

class Recipe < ActiveRecord::Base
    has_many :instructions, -> { order('position ASC') }, autosave: true, class_name: 'RecipeInstruction'
end

I set the position attribute in the recipe controller. There are two disadvantages with this solution:

  1. I have to set the position attribute of the instructions everywhere, where I create instructions.
  2. The order statement only affects fetches from the db. When I change the position attribute in not persisted recipes/instructions, it does not affect the order.

In other programming languages I would override getter and setter of this relation. Which Operators of the association I have to override to cover all possibilities of adding an object to the relation?

PS: I do know that I can override the << operator of the association with:

has_many :instructions, -> { order('position ASC') }, autosave: true, class_name: 'RecipeInstruction' do
    def << *args
    end
end

But the = Operator can't be overwritten this way, can it?

Edit: Now I know how to override the = Operator. Do I have to override all possibilities or is there a method that is called by every operator to add an instruction, like push? And how can I force a "reload" of the related objects when I change the position attribute of one or more instructions?

PPS: Overriding the setter for instructions I tried both variants, but none was called by rails after submitting the form. But the instructions where assigned to the recipe. So there must be another option so set it/to override the setter:

has_many :instructions, -> { order('position ASC') }, autosave: true, class_name: 'RecipeInstruction' do
    def instructions=(instructions)
        raise error
    end
end

has_many :instructions, -> { order('position ASC') }, autosave: true, class_name: 'RecipeInstruction'

def instructions=(instructions)
        raise error
end

PPPS: Solution for setting the position

The first step is taken. I think this is a good solution for initializing the position attribute:

has_many :instructions, -> { order('position ASC') }, autosave: true, class_name: 'RecipeInstruction', before_add: :initialize_instruction


def initialize_instruction(instruction)
    instruction.position = instructions.length
end

Upvotes: 3

Views: 217

Answers (3)

fvosberg
fvosberg

Reputation: 697

Thanks to all your help. I finally found a good solution:

https://github.com/swanandp/acts_as_list

It is a plugin which solves the problem very well.

Edit: But only for persisted records. I assume that is 'active record style'.

Upvotes: 1

Richard Peck
Richard Peck

Reputation: 76774

I think you have the answer already; to give something which may help, you have access to the proxy_association objects when you extend the has_many association:

has_many :instructions, -> { order('position ASC') }, autosave: true, class_name: 'RecipeInstruction' do
    def x
       proxy_association.target
    end
end

According to these docs, you'll also get access to record.association(:name) objects:

@recipe = Recipe.find x
@recipe.instructions = @recipe.association(:instructions).target #-> returns collection of instructions

This will give you access to an array of the instructions, from which you'll be able to extract the positions:

@positions = @recipe.association(:instructions).target.select { |k,v| key.to_s.match(/^position\d+/) }

There's also a reload method:

person.pets.reload # fetches pets from the database
# => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]

Sadly, I don't have anything for interjecting into the << / .destroy methods.

Upvotes: 1

Umang Raghuvanshi
Umang Raghuvanshi

Reputation: 1258

You can write getters/setters in Rails. For example, to write a getter and setter for your Recipe object (in your model),

def position
  where('some req').order('some way')
end

def position=(arg1)
  self.something = arg1
end

That's how you override the = operator.

Upvotes: 0

Related Questions