Search code examples
ruby-on-railsrubypostgresqlactiverecordsinatra-activerecord

Join table has two plural words. How to properly name models and migrations; in order to destroy orphans on join-table. Active Record


Okay, setting aside the fact that asking how to "destroy orphans" on a chat board is just evil, here is my technical question:

I am making a simple blog-site, using Activerecord and Sinatra (and I am new to both). I have a "many to many" relationship between a Post model and a Tag model, and have been getting errors that I was not able to sort out. Finally, I arranged the models and migration so that I am not getting errors; however, when I destroy an instance of a tag, the the associations on the "posts_tags" table are left in place (existing as orphans), and I want to know how to destroy them (cue evil music).

Can anyone assist me with this?

Here are my migration for all three tables:

class CreatePostsTable < ActiveRecord::Migration[5.2]
  def change
    create_table :posts do |t|
      t.string :title, default: 'title here'
      t.string :content, default: 'content here'
      t.belongs_to :blog, index: true
      t.timestamps
    end
  end
end

class CreateTagTable < ActiveRecord::Migration[5.2]
  def change
    create_table :tags do |t|
      t.string :name, unique: true
    end
  end
end

class CreatePostsTagsTable < ActiveRecord::Migration[5.2]
  def change
    create_table :posts_tags, :id => false do |t|
      t.belongs_to :tag, index: true
      t.belongs_to :post, index: true
      # t.integer :post_id
      # t.integer :tag_id
    end
  end
end

And here are my three models:

file-name: Post_Tag (I have named, and re-named this class and file-name over the course of a couple days).

class PostsTag < ActiveRecord::Base
   # self.table_name = "posts_tags"  #had to add this line for seed file not to give errors
    belongs_to :post
    belongs_to :tag
end

class Post < ActiveRecord::Base
    belongs_to :blog
    has_many :posts_tags
    has_many :tags, :through => :posts_tags
    has_many :comments, dependent: :destroy  
end

class Tag < ActiveRecord::Base
    has_many :posts_tags
    has_many :posts, :through => :posts_tags
    # has_and_belongs_to_many :posts, :through => :posts_tags
end

This configuration is not giving me error when I search for a post's tags, or a tag's posts, BUT when I do @tag = Tag.find_by_id(1) @tag.destroy the "posts_tags" table is left still having all of the tag_id == 1 rows still there. When I try to destroy them, I get errors (which I think is because that would also destroy the associations).

Does anyone know how to fix this? Is it okay to simply remove the PostsTag model all together?

The Activerecord documentation uses an example where the join-table is a single word, so I wasn't able to find the answer there.

I saw that some people were simply not making a model for the join-table. When I removed my join-table model, I got this error:

@post = Post.find_by_id(1)
@post.tags.each do |tag|    
    p tag.name    
end

NameError at / uninitialized constant Tag::PostsTag

Thanks for any help!

#

Phase Two of question:

After the tip that I could try adding this code to my models:

class Post < ActiveRecord::Base
    has_many :posts_tags, dependent: :destroy
class Tag < ActiveRecord::Base

I am running this in my server/app file:

delete "/tag/destroy" do
        body = JSON.parse request.body.read  # body: {id: number} or {name: string}
        @tag_to_destroy = Tag.where(body)[0] 
        @tag_to_destroy.destroy
redirect "/tag"
end

I am sending this request via postman:

http://localhost:4567/tag/destroy
body of request:
{
    "id": 12
}

And I am getting this error: (even though there is are rows in the posts_tags database with tag_id = 12, post_id = variousNumbers):

== Sinatra (v2.0.1) has taken the stage on 4567 for development with backup from Puma Puma starting in single mode... * Version 3.11.4 (ruby 2.5.1-p57), codename: Love Song * Min threads: 0, max threads: 16 * Environment: development* Listening on tcp://localhost:4567 Use Ctrl-C to stop D, [2018-05-07T10:54:19.604906 #99099] DEBUG -- : Tag Load (0.5ms) SELECT "tags".* FROM "tags" WHERE "t ags"."id" = $1 [["id", 12]] D, [2018-05-07T10:54:19.617955 #99099] DEBUG -- : (0.3ms) BEGIN D, [2018-05-07T10:54:19.633736 #99099] DEBUG -- : PostsTag Load (1.5ms) SELECT "posts_tags".* FROM "pos ts_tags" WHERE "posts_tags"."tag_id" = $1 [["tag_id", 12]] D, [2018-05-07T10:54:19.642305 #99099] DEBUG -- : PostsTag Destroy (1.6ms) DELETE FROM "posts_tags" WHERE "posts_tags"."" IS NULL D, [2018-05-07T10:54:19.642685 #99099] DEBUG -- : (0.2ms) ROLLBACK 2018-05-07 10:54:19 - ActiveRecord::StatementInvalid - PG::SyntaxError: ERROR: zero-length delimited identifier at or near """" LINE 1: DELETE FROM "posts_tags" WHERE "posts_tags"."" IS NULL ^ : DELETE FROM "posts_tags" WHERE "posts_tags"."" IS NULL: /Users/maiya/.rvm/gems/ruby-2.5.1/gems/activerecord-5.2.0/lib/active_record/connection_adapters/postgresql_adapter.rb:603:in `async_exec'

Question update:

The join-table migration was previously:

  create_table :posts_tags  :id => false do |t|
      t.belongs_to :tag, index: true   
       # comment: the above line creates: t.integer :post_id
      t.belongs_to :post, index: true
       # comment: the above line creates: t.integer :tag_id  
  end

Solution

  • All is OK now with your migrations and models, you just need to change in the Post and Tag models:

    has_many :posts_tags
    

    to

    has_many :posts_tags, dependent: :destroy
    

    In this case after you destroy any tag or post all associated posts_tags are destroyed too, it will ensure referential integrity of your app and prevent errors.

    Also, it is good idea to rollback migrations, add foreign_key: true option to t.belongs_to lines, and migrate it again. This option will provide referential integrity on db level.

    UPDATE:

    has_and_belongs_to_many (HABTM) doesn't work with through option, you mixes it up with has_many association. Please, read about it more in guides

    To make it all working you need to:

    • remove :id => false option from CreatePostsTagsTable migration
    • remigrate or drop and recreate db
    • change has_and_belongs_to_many <...>, :through => :posts_tags to has_many <...>, :through => :posts_tags in both Tag and Post models

    P.S. By convention all file names in ruby are in lower snake case, it should be `posts_tag.rb', not 'Posts_Tag.rb'