0

I'm attempting use the REST API in my rails (version 3.2.9) app.

I am POSTing a json packet to a standard rails scaffolded controller, which includes details for the model itself, and nested children.

The parent model and its children are successfully inserted into the database. However, the response from the POST doubles the nested children.

Why are the nested associations being doubled in the POST response? I have been trying to get this right for the last few days... it is driving me nuts.

Specific examples

JSON request

POST http://localhost:3000/audio_events HTTP/1.1
Host: localhost:3000
Connection: keep-alive
Content-Length: 432
Accept: application/json, text/plain, */*

{
    "audio_event": {
        "start_time_seconds": 0.05,
        "end_time_seconds": 15.23,
        "low_frequency_hertz": 1000,
        "high_frequency_hertz": 8753,
        "audio_event_tags_attributes": [
            {
                "tag_id": "-1"
            },
            {
                "tag_id": "-2"
            }
        ],
        "audio_recording_id": "1bd0d668-1471-4396-adc3-09ccd8fe949a"
    }
}

JSON response

HTTP/1.1 201 Created
Location: http://localhost:3000/audio_events/27
Content-Type: application/json; charset=utf-8
X-Ua-Compatible: IE=Edge
Etag: "c79ccdf981a9fadad3a8b08c3a878e8e"
Cache-Control: max-age=0, private, must-revalidate
Content-Length: 924
Server: WEBrick/1.3.1 (Ruby/1.9.3/2012-04-20)
Date: Mon, 26 Nov 2012 00:27:47 GMT
Connection: Keep-Alive

{
    "created_at": "2012-11-26T00:27:32Z",
    "creator_id": 1,
    "deleted_at": null,
    "deleter_id": null,
    "end_time_seconds": "15.23",
    "high_frequency_hertz": "8753.0",
    "id": 27,
    "is_reference": false,
    "low_frequency_hertz": "1000.0",
    "start_time_seconds": "0.05",
    "updated_at": "2012-11-26T00:27:32Z",
    "updater_id": 1,
    "audio_event_tags": [
        {
            "audio_event_id": 27,
            "created_at": "2012-11-26T00:27:32Z",
            "creator_id": 1,
            "tag_id": -1,
            "updated_at": "2012-11-26T00:27:32Z",
            "updater_id": 1
        },
        {
            "audio_event_id": 27,
            "created_at": "2012-11-26T00:27:32Z",
            "creator_id": 1,
            "tag_id": -2,
            "updated_at": "2012-11-26T00:27:32Z",
            "updater_id": 1
        },
        // DUPLICATES ARE HERE
        {
            "audio_event_id": 27,
            "created_at": "2012-11-26T00:27:32Z",
            "creator_id": 1,
            "tag_id": -1,
            "updated_at": "2012-11-26T00:27:32Z",
            "updater_id": 1
        },
        {
            "audio_event_id": 27,
            "created_at": "2012-11-26T00:27:32Z",
            "creator_id": 1,
            "tag_id": -2,
            "updated_at": "2012-11-26T00:27:32Z",
            "updater_id": 1
        }
    ],
    "audio_recording": {
        "id": 1,
        "uuid": "1bd0d668-1471-4396-adc3-09ccd8fe949a"
    }
}

Models

Many to many model

class AudioEventTag < ActiveRecord::Base
  belongs_to :audio_event
  belongs_to :tag
  accepts_nested_attributes_for :audio_event

  attr_accessible :audio_event, :tag, :tag_id

  stampable
  belongs_to :user, :class_name => 'User', :foreign_key => :creator_id

  validates_uniqueness_of :audio_event_id, :scope => :tag_id
end

The tag model

class Tag < ActiveRecord::Base
  has_many :audio_event_tags
  has_many :audio_events, :through => :audio_event_tags
  accepts_nested_attributes_for :audio_events, :audio_event_tags

  attr_accessible :is_taxanomic, :text, :type_of_tag

  stampable
  belongs_to :user, :class_name => 'User', :foreign_key => :creator_id
  acts_as_paranoid
  validates_as_paranoid
end

The audio event model

class AudioEvent < ActiveRecord::Base
  belongs_to :audio_recording

  has_many :tags, :through => :audio_event_tags, :uniq => true

  has_many :audio_event_tags
  accepts_nested_attributes_for :audio_event_tags

  attr_accessible :audio_recording_id, :end_time_seconds, :high_frequency_hertz, :is_reference,
                  :low_frequency_hertz, :start_time_seconds,
                  :tags_attributes, :audio_event_tags_attributes

  stampable
  belongs_to :user, :class_name => 'User', :foreign_key => :creator_id
  acts_as_paranoid
  validates_as_paranoid

  # validation
  validates :audio_recording, :presence => true

  validates :start_time_seconds, :presence => true, :numericality => { :greater_than_or_equal_to  => 0 }
  validates :end_time_seconds, :numericality => { :greater_than_or_equal_to  => 0 }

  validates :low_frequency_hertz, :presence => true, :numericality => { :greater_than_or_equal_to  => 0 }
  validates :high_frequency_hertz,  :numericality => { :greater_than_or_equal_to  => 0 }

  # json formatting
  def as_json(options={})
    super(
        :include =>
            [
                :audio_event_tags,
                :audio_recording  => {:only => [:id, :uuid]}
            ],
        :except => :audio_recording_id
    )
  end
end

Updates

As requested in comments, the code for my action in the controller

# POST /audio_events
# POST /audio_events.json
def create
  @audio_event = AudioEvent.new(params[:audio_event])
  #@audio_event.audio_event_tags.each{ |aet|  aet.build() }

  #@audio_event.audio_event_tags.count.times { @audio_event.audio_event_tags.build }

  respond_to do |format|
    if @audio_event.save

      format.json { render json: @audio_event, status: :created, location: @audio_event }
    else
      format.json { render json: @audio_event.errors, status: :unprocessable_entity }
    end
  end
end

And, I tried removing the stampable and acts_as_paranoid macros to no effect.

Updates 2

If I try adding 3 nested attributes, I get 6 back. 4 gives back 8, and 5 gives back 10.

  • I posted an answer regarding one use that can be made of ̉̉`as_json` : http://stackoverflow.com/questions/3872236/rails-object-to-hash/8429140#8429140 Maybe this can help out finding what your problem is. I will try implementing your code later today and see if I can find out what's happening. – Raf Nov 29 '12 at 07:08
  • Well I did a quick implementation of your models and i can't seem to get duplicates. It works as expected. Maybe the problem is not in your models but in the way you call out your model serialization. Could you update your answer with that code? – Raf Nov 29 '12 at 08:57
  • i just wonder, did you try to see what happens when you comment out the stampable and acts_as_paranoid macros ? maybe they get in your way. Both fiddle with the internals of active record... – m_x Dec 02 '12 at 00:05
  • My apologies for not responding sooner - I had expected to receive email updates (which I didn't get). I tried both suggestions in the comments, no luck. – Anthony Truskinger Dec 03 '12 at 06:24
  • Are you sure the `audio_event_tags` are not doubled in the database? – nathanvda Dec 03 '12 at 10:10
  • The other check would be to add a migration add_index :audio_event_tags, [:audio_event_id,:creator_id, :tag_id], :unique => true – so_mv Dec 03 '12 at 12:05
  • @nathanvda, triple checked not duplicated in db. Plus once I do a separate get on the audio_event it returns the correct number of audio_event_tags. – Anthony Truskinger Dec 04 '12 at 01:39
  • @so_mv I tried adding the index you suggested, and another couple of variants. No luck. – Anthony Truskinger Dec 04 '12 at 01:40
  • Looks like the json serialization issue. If separate get is working you could try @audio_event.audio_event_tags.reload before format.json in the controller. – so_mv Dec 04 '12 at 01:56
  • THAT SEEMS TO WORK! AWESOME. If you could add this as an answer, and help me out by explaining why this happens... or even better point my frustrated person to some documentation? The downside to this is that there seems to be an additional select query for this method... – Anthony Truskinger Dec 04 '12 at 02:08
  • I know that when Rails is building forms for nested models and submits it then the structure looks like this: `audio_event_tags_attributes: { "0": { tag_id: "-1" }, "1": { tag_id: "-2" } }`. Maybe you can try it like that instead – DanneManne Dec 04 '12 at 02:13
  • @DanneManne, I tried that format (without the `@audio_event.audio_event_tags.reload`) and the same duplication behavior occurred. – Anthony Truskinger Dec 04 '12 at 03:24
  • I recommend using active_model_serializers https://github.com/rails-api/active_model_serializers instead of overriding as_json. You'll be way more sane reading a class than a hash. – joshuacronemeyer Dec 05 '12 at 09:07
  • @joshuacronemeyer, I tried active_model_serializers and although I quite like that gem (and I am going to keep using it), It didn't help the duplication problem. – Anthony Truskinger Dec 06 '12 at 00:50

1 Answers1

0

Reposting from comments.

Looks like the json serialization issue. If separate get is working you could try @audio_event.audio_event_tags.reload before format.json in the controller.

-- It is certainly strange. Inmemory details for audio_event_tags got messed up at @audio_event.save line. Does AudioEvent have after_save callbacks?. If there are no callbacks then I don't know how that can happen. Need to reproduce it. It is a serious problem to deal with if it happens for all nested attributes.

so_mv
  • 3,939
  • 5
  • 29
  • 40
  • This seems to be the only thing that worked. It is a little annoying that I have to an extra query to reload the audio_event_tags... and I don't understand why it needs to be like this... at least I can move on. FYI @so_mv, both AudioEvent and AudioEventTag have one after_save callack set... from debugging, the after_save prop has an array, with one item in it, of the same class. I tried clearing these arrays to no effect. – Anthony Truskinger Dec 06 '12 at 00:57
  • If you want to avoid the db call you can try @audio_event.audio_event_tags.uniq!{|aet| aet.tag_id} instead of .reload. Note that uniq! works only on Ruby 1.9.3. v1.8.7 also has uniq but does not use the block for comparison. – so_mv Dec 06 '12 at 03:19