0

I have a Question that has_many :answers (just like SO). I also want each question to have only 1 accepted_answer, so I just added an :accepted attribute to the Answer model that is simply a boolean.

So now, to get the accepted answer for my question, I have written a method on my model that just does this:

  def accepted_answer
    answers.where(accepted: true)
  end

That allows me to do question.accepted_answer and it returns an ActiveRelation object like you would expect.

Nothing fancy. Simple and effective.

However, what I want to ensure though is that there can only be one answer on each question that is accepted: true at any moment in time.

What's the best way to approach this?

I thought about using a validator, but I couldn't find one that handled associated objects in this way. There are some that have bits & pieces that are interesting, but I can't quite fit all the pieces together. For instance, presence is interesting as is absence and validates_with (but this last one feels too heavy).

Suggestions?

marcamillion
  • 32,933
  • 55
  • 189
  • 380

1 Answers1

1

Most likely the best way would be to use after_add callback (an example here), which would set to false all your existing accepted records via update_all and the latest answer with accepted set to true. It all depends on your logic.

You can also employ some other callbacks such as before_save, before_update and such with the similar functionality depending on your application specifics.

It is not quit a validation, but it will effectively maintain the required state of your model. Besides, the purpose of the validations to warn you when something is not valid, but I guess you want to save your object without such failures, and just enforce a single accepted answer.

Let me know in case you want to stop adding answers after the first one was accepted. In this case it would require a different functionality.

Community
  • 1
  • 1
  • No need to stop adding answers after the first one is accepted. The functionality is very similar to SO. I just need there to be only one accepted_answer at any moment in time. – marcamillion Jun 22 '16 at 11:46
  • 1
    Then you should be good with what I have described. I am using something similar to create a primary image among set of images. –  Jun 22 '16 at 11:48
  • 1
    Interesting. Never knew about `update_all`. That's a good idea actually. In my `accept_answer` action on my controller, I could simply just do something like `q.answers.update_all(accepted: false)`, and then just assign the one I want to be `@answer.update(accepted: true)`. Something like that is what you were thinking? – marcamillion Jun 22 '16 at 11:48
  • @marcamillion: Yes, that is exactly what I was implying. `update_all` is very effective because it generates a single update statement, and it does not trigger callbacks on affected models (assuming that your are OK with that). –  Jun 22 '16 at 11:51
  • 1
    @marcamillion: One more thing. Be careful when you do `@answer.update(accepted: true)` because it may trigger callbacks and you may get into an infinite loop. You might want to try `update_column` instead (http://apidock.com/rails/ActiveRecord/Persistence/update_column), which also should not triggers any callbacks as far as I recall. –  Jun 22 '16 at 11:55
  • Yep this is perfect for what I want. Thanks for the tip! – marcamillion Jun 22 '16 at 11:56
  • @marcamillion: Also, after thinking about your statement `@answer.update(accepted: true)` I started to suspect that your are doing this logic in controller. Is it correct? I would advise to try moving it to the Answer model. Why? Because you may have another controller in the future which would do about the same thing, and you will have to duplicate the code. You want to make your models smart enough to be salf-maintainable, so you may try to create a method in your Answer to do `project.accepted_answers.update_all(accepted_answer: false); self.update_column(accepted_answer: true)`. –  Jun 22 '16 at 12:05