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.