How Do I: Avoid an Infinite Loop using Before Save and After Save?

I have an entity that looks something like this:

journal   author_first_name   author_last_name   journal_name   journal_email_address

where   journal_name = author_first_name + ' ' + author_last_name   journal_email_address = journal_name + '-' id

I had journal_name and journal_email_address implemented as attributes on the Journal model, but the time came along where, for various reasons, it has become valuable to also store that information in the database. So, what I thought I'd do, in app/models/journal.rb:

  def make_journal_name     self.journal_name = self.author_first_name + ' ' + self.author_last_name   end

  def make_inbound_email     self.inbound_email = self.journal_name.gsub(/[^a-zA-Z0-9]/, '') + '-' + self.id.to_s   end

  def before_save     make_journal_name     make_inbound_email   end

which works fine in the case of an update of an existing record. However, it does not work in the case of a creation of a new record because before saving self.id does not exist. The result was my e- mail addresses are looking like "SallyJones-" as opposed to "SallyJones-64".

Okay, I thought, "since I need an ID to work with I'll move this to after_save," like so:

  def after_save     make_journal_name     make_inbound_email     self.save   end

but that creates an infinite loop.

At this point, I'm sort of stumped for what to do...thoughts on how I could make this work?

Scott wrote:

I have an entity that looks something like this:

journal   author_first_name   author_last_name   journal_name   journal_email_address

where   journal_name = author_first_name + ' ' + author_last_name   journal_email_address = journal_name + '-' id

I had journal_name and journal_email_address implemented as attributes on the Journal model, but the time came along where, for various reasons, it has become valuable to also store that information in the database. So, what I thought I'd do, in app/models/journal.rb:

  def make_journal_name     self.journal_name = self.author_first_name + ' ' + self.author_last_name   end

  def make_inbound_email     self.inbound_email = self.journal_name.gsub(/[^a-zA-Z0-9]/, '') + '-' + self.id.to_s   end

  def before_save     make_journal_name     make_inbound_email   end

which works fine in the case of an update of an existing record. However, it does not work in the case of a creation of a new record because before saving self.id does not exist. The result was my e- mail addresses are looking like "SallyJones-" as opposed to "SallyJones-64".

Okay, I thought, "since I need an ID to work with I'll move this to after_save," like so:

  def after_save     make_journal_name     make_inbound_email     self.save   end

but that creates an infinite loop.

At this point, I'm sort of stumped for what to do...thoughts on how I could make this work?

I don't really understand why you need these columns as they could easily be computed on the fly by the model with simple methods.

If you really, really want to do this, you can update the record with raw SQL instead of using self.save. From memory: execute("UPDATE #{table_name} SET inbound_email = '#{make_inbound_email}', journal_name = '#{make_journal_name}' WHERE id = '#{id}'") It would work with your current make_* methods as they return the value they set in the model which have the added benefit that your model is in sync with the DB.

But I'd really like to know why you have to break database basic normalization rules (don't put twice the same data in your database to avoid inconsistencies, should be part of the "first normal form" if my old DB courses serve me right).

Lionel

Thank you for your response.

Actually, that doesn't quite solve my problem because my original intention of using before_save worked for updates, and the SQL above is an UPDATE. The real problem is when I'm creating a new object, i.e. doing an INSERT. I could change the SQL to an INSERT, but I'd really have to make sure that didn't screw anything up.

As far as the need: I do lookups based on those attributes, the email address especially, and it will speed things up (and eliminate errors) if I can do an lookup on an indexed column as opposed to parsing the string to figure out what to look up.

Another option is to call make_* methods from the #create and #update methods in my controllers, although that seems like an odd place for them.

Scott

Scott wrote:

Thank you for your response.

Actually, that doesn't quite solve my problem because my original intention of using before_save worked for updates, and the SQL above is an UPDATE.

Right, the insert is already done at this point. So the only task left is to fill the missing columns which only an update can do...

  The real problem is when I'm creating a new object, i.e. doing an INSERT. I could change the SQL to an INSERT, but I'd really have to make sure that didn't screw anything up.    It would because the INSERT is already done: you'll get an SQL exception.

As far as the need: I do lookups based on those attributes, the email address especially, and it will speed things up (and eliminate errors) if I can do an lookup on an indexed column as opposed to parsing the string to figure out what to look up.    Maybe, depends on the details, if you can pre-process the query to infer a query on the components, it should be the fastest way (query on smaller columns are always faster). If you can't, then... you can't :slight_smile: Depending on your database on you can even index expressions, so you could make find_sql with conditions that look like "(col1 || ' ' || col2) LIKE 'escaped_querystring'" and have the database index concat(col1, ' ', col2) for you (PostgreSQL supports this since ages ago). This should be the more robust and fastest way.

Lionel

Scott wrote:

  def after_save     make_journal_name     make_inbound_email     self.save   end

but that creates an infinite loop.

Instead of "save" use "update_without_callbacks"

Mark...update_without_callbacks does exactly what I needed. Thanks!

However, I can't find it mentioned anywhere in the documentation. I grepped the code and found that it's set for #:nodoc.

a) how could i have known this exists without knowing every line of code? b) how did you know this exists? c) why is this method, and others, not included in the docs? someone had to choose to add #:nodoc to the code...why did they choose to do that?

Scott wrote:

Mark...update_without_callbacks does exactly what I needed. Thanks!

However, I can't find it mentioned anywhere in the documentation. I grepped the code and found that it's set for #:nodoc.

a) how could i have known this exists without knowing every line of code? b) how did you know this exists? c) why is this method, and others, not included in the docs? someone had to choose to add #:nodoc to the code...why did they choose to do that?

update_without_callbacks is created using alias_method_chain, so it's not easy to add to the rdocs, but it should be.