Search code examples
ruby-on-railsrubyruby-on-rails-4activemodel

What is the most comprehensive way to set an attribute on a model in a has_many association using an attribute of the model it belongs_to?


I have two ActiveModels, MultivariateExperiment which has_many MultivariateExperimentVariant. Conversely, a MultivariantExperimentVariant belongs_to a MultivariateExperiment.

MultivariateExperiment has an attribute experiment_name.

MultivariantExperimentVariant has the attributes name and weighting.

I'd like the variants' name to be in the format experiment_name_0, experiment_name_1, etc. For instance, given the following MultivariateExperiment:

mve = MultivariateExperiment.create({ experiment_name: 'user_signup' })

I'd like to have a programmatic way of having the associated variants be:

mve.multivariate_experiment_variants.create({ weighting: 1 }) # expected name: "user_signup_0"

mve.multivariate_experiment_variants.create({ weighting: 1 }) # expected name: "user_signup_1"

mve.multivariate_experiment_variants.create({ weighting: 2 }) # expected name: "user_signup_2"

I initially thought about putting this in an after_commit callback but was told in code review to avoid it as that callback is finnicky (not sure why)

I took a look at some other callbacks but none of them seem comprehensive enough to cover the myriad of ways an association can be created, such as the following:

# 1st approach
mve.multivariate_experiment_variants.create({ weighting: 1 })

# 2nd approach
variant = MultivariateExperimentVariant.create({ weighting: 1 })
mve << variant
mve.save

# 3rd approach
mve.multivariate_experiment_variants.build({ weighting: 1 })
mve.save


# etc. etc.

So, given the various ways to create associations, are there any mechanisms or approaches that can successfully compute an attribute of a Model in a has_many relationship using an attribute of the Model it belongs to?


Solution

  • In the MultivariateExperimentVariant model you could do...

    after_save :set_name
    
    private
    
    def set_name
      # Assuming you have a required belongs_to
      update(name: "#{multivariate_experiment.name}_#{multivariate_experiment.multivariate_experiment_variants.length - 1}")
    end
    

    You wouldn't want to use after_commit unless you specify the on: condition

    after_commit :set_name, on: [:create, :update]
    

    Otherwise, it will try to set name after the record is destroyed.

    Side note: It may be better to check the index instead...

     update(name: "#{multivariate_experiment.name}_#{multivariate_experiment.multivariate_experiment_variants.index(self)}")