views:

368

answers:

3

Part of my RoR application is responsible for managing web site designs portfolio. One web site design can have many images associated with it. One image can be associated only with one design. I use has_many statement with :through parameter to connect images with design through join table. And when image is deleted associated entry in join table must be deleted. So i have next models For images:

class  Image < ActiveRecord::Base
  has_one :images_site_designs , :class_name => "ImagesSiteDesigns" , :dependent => :destroy
  has_one :site_design , :through => :images_site_designs
end

For site_designs:

class SiteDesign < ActiveRecord::Base
  belongs_to :client
  has_many :images_site_designs , :class_name => "ImagesSiteDesigns"
  has_many :images , :through => :images_site_designs
end

And join table images_site_designs:

class ImagesSiteDesigns < ActiveRecord::Base
  belongs_to :image 
  belongs_to :site_design
end

Creating new images for site_designs is ok, so folowing code works fine:

   @site_design = SiteDesign.find(params[:id])
   @site_design.images << Image.new(params[:image])

But when i try to delete image next error appears:

 ActiveRecord::StatementInvalid in ImagesController#destroy

Mysql::Error: Unknown column 'id' in 'where clause': DELETE FROM `images_site_designs` WHERE `id` = NULL

It seems that rails use wrong column name for querying images_site_designs join table. How can i fix it?

upd:

image_controller functions that deletes image:

  def destroy
    @image = Image.find(params[:id])
    @image.destroy

    respond_to do |format|
      format.html { redirect_to(images_url) }
      format.xml  { head :ok }
    end
  end

migrations:

class CreateImages < ActiveRecord::Migration
  def self.up
    create_table :images do |t|
      t.string :url
      t.string :name
      t.text :description

      t.timestamps
    end
  end

  def self.down
    drop_table :images
  end
end
class CreateSiteDesigns < ActiveRecord::Migration
  def self.up
    create_table :site_designs do |t|
      t.string :name
      t.text :concept
      t.text :description
      t.integer :client_id

      t.timestamps
    end
  end

  def self.down
    drop_table :site_designs
  end
end

class CreateImagesSiteDesigns < ActiveRecord::Migration
  def self.up
    create_table :images_site_designs , :id => false do |t|
      t.integer :image_id
      t.integer :site_design_id
    end
  end

  def self.down
    drop_table :images_site_designs
  end
end
A: 

If you want that one image can only be associated with one design, why then you use a has_and_belongs_to relationship (you use a n:m table!)

I would refactor to following:

1) Migrate your Images model and add an Site_design_id attribute

class CreateImages < ActiveRecord::Migration
  def self.up
    create_table :images do |t|
      t.string :url
      t.string :name
      t.text :description
      t.integer :site_design_id

      t.timestamps
    end
  end

  def self.down
    drop_table :images
  end
end

2) Drop the ImagesSiteDesigns-Migration

drop_table :images_site_designs

3) Change the models to:

class SiteDesign < ActiveRecord::Base
  belongs_to :client
  has_many :images
end

class  Image < ActiveRecord::Base
  belongs_to :site_design
end

So you end up with an 1:n relationship and that should be the best solution for your specification.

You can acces your models as following:

Image.first.site_design
=> <SiteDesign #id:...>
SiteDesign.first.image
=> <Image #id...>

SiteDesign.image = Image.new(params[:image])
...
Lichtamberg
I added migrations and controller's function in post.
ebsbk
Maybe this is you solution
Lichtamberg
Thank you for suggestion, but in future i want to add other stuff (something like gallery) that will be connected with images and may be it will have many to many relationship, so images table's structure must be clear.
ebsbk
Hm... your tables structure is clear with this code above...! (and you can change the above structure again in the future!) What do you mean exactly?
Lichtamberg
For altering your database and migrate existing data, you can use the reset_column_information for your models... Look at http://apidock.com/rails/ActiveRecord/Base/reset_column_information/class
Lichtamberg
Lets imagine that i need to store information associated with images gallery. One image can have ONE galley and one gallery can have many images. To connect images table with galleries i need to add another filed to images table - gallery_id. So images will have id, description, concept, site_design_id, gallery_id fields. One new association results in another one field, and you need to refactor images table again.
ebsbk
Like you said, you would only have to add the gallery_id attribute to your image model. With migrations this should be no problem. I suggest you do use my solution, because its the best practice and doesnt make problems like your solution. Beside this its ok with the normal forms: http://en.wikipedia.org/wiki/Database_normalization#Normal_forms. You should produce a clear and maintainable database structure/code and not a wishi-washi do-it-yourself-solution. But its your decision...
Lichtamberg
+1  A: 

Can't really answer your question because it is not shown what code triggers the error. Presumably a call to destroy on an Image instance, but can't be sure.

However, it doesn't look like you need that join model anyways. The requirement seems to be met by the following:

class SiteDesign < ActiveRecord::Base
  belongs_to :client
  has_many :images
end

class Image < ActiveRecord::Base
  belongs_to :site_design
end

Of course, this would require a migration (drop join table and add site_design_id to the images table), but seems like a cleaner solution. Any reason not to do this?

hgimenez
I added migrations and controller's function code in post.In future i want to add other stuff (something like gallery) that will be connected with images, so images table's structure must be clear.
ebsbk
Worry about the future in the future. Solve the problem at hand.
hgimenez
Looking at your migrations now, I note that you are saying :id => false on the join model. If you use :id => false, then it should be a pure join model without timestamps (just two columns/references). Remove the :id => false and your code should work. Otherwise, use has_and_belongs_to_many instead of has_many :through.
hgimenez
Thank you. Solved the problem adding has_and_belongs_to_many relationship instead of using Model like join table.
ebsbk
+1  A: 

The problem here is that you have a migration which makes your image_site_design NOT be a model (no id column), but you are then making an ActiveRecord model for it. In order for what you are doing to work you need to actually have an id column in your join table.

Shebanator
Thank you. Solved the problem adding has_and_belongs_to_many relationship instead of using Model like join table.
ebsbk