Skip to main content

Best way to create or save a record (with associations) as a template

General • Asked by tquill

I'm curious as to what people think is the best to create a new record, or save an existing record... as a template. The template would be used as a starting point to duplicate that record easily and quickly in the future (with associated records).

For example, let's say I have a survey application with surveys, which have many questions, which have many answers. Each user creates their own surveys, but each user might have a few different survey templates (pre-populated questions and answers) they'd like to start from and then edit... rather than creating an entirely new survey each time from scratch.

I see two different approaches for this...

The first is to save existing surveys as templates (still not sure how though), and then create survey's from those.

The second is to create the templates first, then create surveys from those templates.

I think both approaches are useful and I'd like to get advice on how to implement both of them.

Thanks for any help.


Templates are a definitely interesting topic. I spent some time making templates in the past and I believe I ended up trying a couple different things. One approach I had a templates table, but it felt like there was a lot of duplication going on (two tables, same columns for example). The other was to mark records as templates and we'd just copy a few fields over to the new record. That ended up working better for what we were doing because it was pretty simple.

I personally like just operating off an existing record because you can just say "Okay, let's dup this, including it's children, but only keep these certain fields, strip out these others" and you don't have to deal with creating multiple tables or anything.

If you go that approach, you can use the .dup method on the instance, so you'd load up the original and then call .dup on it which should generate a copy in memory without an ID attached. You might also need to loop through associated records and dup those as well for the new model. Then you can clean any of the fields you want by setting them to nil or whatever.

The other approach for this same thing is to take the attributes from the original, use except on the hash to strip out those fields, and then create a new record like you normally would in the controller. Either way is effectively the same, this one might be a lot more familiar to most people. .dup wins if you don't need to strip out any fields though because everything's already set automatically.


@chris will love to see an episode on this topic


Chris, I appreciate the answers. I'll try the .dup method. I agree that operating off existing records is the better way to go... much less duplication in the background.


I'm using this the deep_cloneable gem (https://github.com/moiristo/deep_cloneable) and it seems to work pretty well.


I am trying to clone a model which has many has_and_belongs_to_many associations. Is there an easy way to copy those associations?


Yes, using the deep_cloneable gem you can add associated records as arguments.


Thanks Todd. I got rid of HTBM associations and changed all of them to HMT (has many through)


Throwing in some thoughts on this as it's something I've dealt with in the past and am currently working on a project right now!

I agree with Chris's thoughts, that a single table is much easier to maintain, especially when you start dealing with associations and changing column names, etc...

One of the cleanest ways to separate them in your Rails code is to use different classes/models. For instance, let's say you have a workflows table and some of the records are templates based on a is_a_template boolean column.

I would break that up into a few different classes.

class BaseWorkflow < ApplicationRecord
  # code shared between "live" and "template" records
  scope :templates, -> { where(is_a_template: true) }
  scope :not_templates, -> { where(is_a_template: false) }
  has_many :steps
end

class WorkflowTemplate < BaseWorkflow
  before_save { self.is_a_template  = true }
  default_scope { templates }
end

# "live" workflow
class Workflow < BaseWorkflow
  before_save { self.is_a_template  = false }
  default_scope { not_templates }
end

The interesting part is when you have several layers of associations, such as workflow has_many :steps, and step has_many :task,etc... I personally found that using live/template classes for all the children became a burden and it was rather redundant, because you already know they are templates because their parent is a template.

This allows you to put all your core relationships in the BaseWorkflow class and not duplicate them in each of your live/template classes.

I also played with duplicating all my tables and I also tried separating all of the live/template versions into 2 sets of models, and it because overwhelming very quickly and I just had a few core models I was dealing with. Anytime I had some logic change or needed a new method, it was a pain to duplicate it in 2 locations.

My current setup is to only break up the parent record into live/template, and then place any unique code in those classes instead of having conditional logic in the base record. This allows you to breakup the logic only when you need to, which is deciding whether this parent record and it's associations belongs in the live or template category.


@deniel will love to see a demo app on this approach as I can clean my code a lot with this trick. your using a Single Table Inheritance


Daniel,

I agree. I did try having separate models for templates and non-templates (and all of the associated records), and it was a terrible approach. As you mentioned, it was a lot of unnecessary duplication. Now I just use the same model, with a template boolean. I hadn't thought of breaking it up into different classes, but I may look into that if the need arises.

I still use the deep_cloneable gem, but for a record with a lot of children and grandchildren... it can be slow. I've been using another gem (active record import) for those large records... and it's much faster... although it a bit more setup.


Login or Create An Account to join the conversation.

Subscribe to the newsletter

Join 24,647+ developers who get early access to new screencasts, articles, guides, updates, and more.

    By clicking this button, you agree to the GoRails Terms of Service and Privacy Policy.

    More of a social being? We're also on Twitter and YouTube.