rails model has_many :through associations - ruby-on-rails

I am trying to get my relationships worked out but I am having trouble using the associations.
So I have three models Workout, Exercise and WorkoutExercise. A workout should have many exercises and a exercise should have different workouts therefore I wrote:
class Workout < ActiveRecord::Base
has_many :workout_exercises
has_many :exercises, :through => :workout_exercises
end
class Exercise < ActiveRecord::Base
has_many :workout_exercises
has_many :workouts, :through => :workout_exercises
end
class WorkoutExercise < ActiveRecord::Base
belongs_to :exercise
belongs_to :workout
end
I am running some tests but the tests aren't passing once I create a workout, exercise and then join them in the workout_exercise class. It won't let me access the exercises in the workout like this:
Workout.create
Exercise.create
WorkoutExercise.create(:workout => Workout.first, :exercise => Exercise.first)
work = Workout.first
work.exercises.count #This line causes the error: undefined method exercises
My database tables look like this:
class CreateWorkouts < ActiveRecord::Migration
def change
create_table :workouts do |t|
t.string :title
t.text :description
t.float :score
t.timestamps
end
end
end
class CreateExercises < ActiveRecord::Migration
def change
create_table :exercises do |t|
t.string :title
t.text :description
t.float :value
t.timestamps
end
end
end
class CreateWorkoutExercises < ActiveRecord::Migration
def change
create_table :workout_exercises do |t|
t.timestamps
end
end
end
When I run this tests it says exercises is undefined. Does anyone have any ideas?

Ok, so your WorkoutExercises table can't be empty. This is how it should look:
class CreateWorkoutExercises < ActiveRecord::Migration
def change
create_table :WorkoutExercises do |t|
t.integer :exercise_id, :null => false
t.integer :workout_id, :null => false
t.timestamps
end
# I only added theses indexes so theoretically your database queries are faster.
# If you don't plan on having many records, you can leave these 2 lines out.
add_index :WorkoutExercises, :exercise_id
add_index :WorkoutExercises, :workout_id
end
end
Also, you can name this table whatever you'd like, it doesn't have to be WorkoutExercises.
However, if you were using a has_and_belongs_to_many relationship, your table would have to mandatorily be named ExercisesWorkout. Notice how Exercises comes before Workout. The names have to be alphabetically ordered. Don't ask me why, it's just a Rails convention.
So, in this case, you'll do fine with your table being named WorkoutExercises. But if I were you, I'd change it to ExercisesWorkout, just in case, so that you never get it wrong.

Your code looks OK. Bug maybe has_and_belongs_to_many is a better choice. See Choosing Between has_many :through and has_and_belongs_to_many

Related

Self association in rails to keep track of related records

I have a model named Letter and an other one named LetterTracking :
class Letter < ApplicationRecord
has_many :letter_trackings, as: :trackable
end
and:
class LetterTracking < ApplicationRecord
belongs_to :letter
has_many :letter_trackings, as: :trackable
end
this is my create table migration for Letter Tracking:
class CreateLetterTrackings < ActiveRecord::Migration[5.0]
def change
create_table :letter_trackings do |t|
t.integer :trackable_id, default: 0, null: false, unique: true
t.string :trackable_type
t.text :paraph
t.string :status
t.string :assignee
t.belongs_to :letter
t.timestamps
end
end
end
as you can see in below screen shots when I select a tracking record for the second tracking the relation is ok but when ever I add the third letter tracking the second one relation removes and the last one keeps the association.
What I want is to keep the letter tracking in each record not by the last one. I mean some thing like nested records in which I can keep the related records.
any Idea ?
Thank you
First of all as a second thought the polymorphic relation seems quite useless for keeping track in this case. The thing which fits best here is tree based relation I suppose.
this is my LetterTracking.rb
class LetterTracking < ApplicationRecord
belongs_to :letter
has_many :children, class_name: "LetterTracking", foreign_key: "parent_id"
belongs_to :parent, class_name: "LetterTracking"
end
and this is my letter.rb
class Letter < ApplicationRecord
has_many :letter_trackings
end
and finaly the LetterTrackings Migration:
class CreateLetterTrackings < ActiveRecord::Migration[5.0]
def change
create_table :letter_trackings do |t|
t.references :parent, index: true
t.text :paraph
t.string :status
t.string :assignee
t.belongs_to :letter, index: true
t.timestamps
end
end
end
Now I can have the records of the lettertrackings join together like a tree while keeping the letter id in every single record! Yep :)

Rails 4 has_many :through relationship: destroy parent model instance when child model instance count reaches 0

In our Rails 4 app, there are four models:
class User < ActiveRecord::Base
has_many :administrations, dependent: :destroy
has_many :calendars, through: :administrations
end
class Administration < ActiveRecord::Base
belongs_to :user
belongs_to :calendar
end
class Calendar < ActiveRecord::Base
has_many :administrations, dependent: :destroy
has_many :users, through: :administrations
has_many :posts, dependent: :destroy
end
class Post < ActiveRecord::Base
belongs_to :calendar
end
Here are the corresponding migrations:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :first_name
t.string :last_name
t.string :email
t.integer :total_calendar_count
t.integer :owned_calendar_count
t.timestamps null: false
end
end
end
class CreateAdministrations < ActiveRecord::Migration
def change
create_table :administrations do |t|
t.references :user, index: true, foreign_key: true
t.references :calendar, index: true, foreign_key: true
t.string :role
t.timestamps null: false
end
end
end
class CreateCalendars < ActiveRecord::Migration
def change
create_table :calendars do |t|
t.string :name
t.timestamps null: false
end
end
end
class CreatePosts < ActiveRecord::Migration
def change
create_table :posts do |t|
t.references :calendar, index: true, foreign_key: true
t.date :date
t.time :time
t.string :focus
t.string :format
t.string :blog_title
t.text :long_copy
t.text :short_copy
t.string :link
t.string :hashtag
t.string :media
t.float :promotion
t.string :target
t.integer :approval
t.text :comment
t.timestamps null: false
end
end
end
Each time a user quits a calendar, meaning we destroy the corresponding administration, we want to make sure that the following happens:
We check if there are other administrations for the same calendar.
If there are, we do nothing (the user is simply redirected to his dashboard).
But if there aren't any more (because the user was the last one to quit the calendar for example), then we want to destroy the calendar as well.
This should automatically destroy all the posts belonging to this calendar, thanks to the has_many :posts, dependent: :destroy line in our Calendar model, but we guess it would not hurt to check that too.
We are thinking of achieving this through a private clear_calendar method in the Calendar model, that we would use as an after_destroy callback in the Administrations controller:
private
def clear_calendar
#calendar = Calendar.find(params[:id])
unless #calendar.administration.exists?
#calendar.destroy
end
end
Does that make sense?
This is a very sensible approach, and the whole world is happy that you didn't place this logic in a controller action.
One thing to note: by placing this logic in your Calendar model, you're necessarily binding Calendar and Administration together. Perhaps you find that to be ok at this point in time, but a truly object-oriented program, instead of asking another model if it exists, would instead tell a model what it wants, instead of how it wants it done (e.g. delete self if this association doesn't exist).
I would recommend placing this logic in a PORO--perhaps a service--that removes unnecessary coupling from your database-backed objects. Perhaps that'd look something like so:
class Calendar < ActiveRecord::Base
...
private
def clear_calendar
ParentDestructionService.new(self)
end
end
class ParentDestructionService
def initialize(parent)
#parent = parent
end
.....logic goes here.....
end
This way, not only do you truly separate a how process from a class that shouldn't care about Administration, but you now have the capabilities to either stick this in a Sidekiq process, or simply thread it off. Either way, you're way more flexible. Now in the future, you'll be able to send ANY parent through that service, and things will work as intended.

has_one relationship is resulting in ActiveModel::MissingAttributeError , what am I missing here?

I feel like I am overlooking something obvious here. I can create a story model, and a category model, but I can't relate a story to a category.
Here is how I reproduce the error:
s = Story.new(title: "test", picture_url: "www.google.com")
c = Category.last
s.category = c
error: ActiveModel::MissingAttributeError: can't write unknown attribute `story_id'
Story model
class Story < ActiveRecord::Base
has_many :chapters, dependent: :destroy
has_many :users, through: :story_roles
has_one :category
end
Story migration file
class CreateStories < ActiveRecord::Migration
def change
create_table :stories do |t|
t.string :title
t.string :picture_url
t.integer :category_id
t.timestamps
end
end
end
Category model
class Category < ActiveRecord::Base
belongs_to :story
validates_presence_of :body
end
Category migration
class CreateCategories < ActiveRecord::Migration
def change
create_table :categories do |t|
t.string :body
t.timestamps
end
end
end
In your story model, change has_one :category to belongs_to :category. A rule of thumb is if you have a foreign key for a model, you declare the association as belongs_to. In this example, you have category_id in the story model so you use belongs_to :category in the story model. This makes perfect sense since a story should really belong to a category and a category has_many stories.
You miss t.references :story in your migration. The belongs_to method on category requires story_id.
class CreateCategories < ActiveRecord::Migration
def change
create_table :categories do |t|
t.references :story
t.string :body
t.timestamps
end
end
end
You are missing a foreign_key story_id in your Category Model. Add that column in your categories table and migrate it.That would resolve your issue.
Note: Before migrating the changes,roll back the previous migration.
OR
The best way is what #bekicot suggested. Just add t.references :story. This includes story_id,so that it will be added to your categories table by default.

Creating a database relationship that allows for multiple assignments of the same id

I'm really new to RoR so I apologize if I'm not thinking about this right. I have a Report where I need to be able to assign multiple users to that report. A user can be assigned to more than one report and a report can have multiple users. How do I create the database relationship where this would be allowed. I understand how to assign one user to one report but not many users to a single report.
I'd use a joining class to make this happen:
class Report
has_many :assignments
has_many :users :through => :assignments
end
class User
has_many :assignments
has_many :reports, :through => :assignments
end
class Assignment
belongs_to :report
belongs_to :user
end
The class Assignment has two fields: report_id and user_id to create the relationship.
Read the Ruby on Rails Guide to Active Record Associations: http://guides.rubyonrails.org/association_basics.html
I highly recommend you familiarize yourself with the Ruby on Rails guides. They will prove to be an invaluable asset!! For this task the site would be RailsGuides Active Record Associations.
As far as the code goes you want to create three database tables: reports, reports_users, and users, with reports_users being a join table.
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name, :null => false
t.timestamps
end
end
end
class CreateReports < ActiveRecord::Migration
def change
create_table :reports do |t|
t.string :name, :null => false
t.timestamps
end
end
end
class ReportsUsers < ActiveRecord::Migration
def change
create_table :reports_users, :id => false do |t|
t.references :user, :null => false
t.references :report, :null => false
end
end
end
Once you run this migration you need to set up the active record associations in your models.
class User < ActiveRecord::Base
has_and_belongs_to_many :reports
end
class Report < ActiveRecord::Base
has_and_belongs_to_many :user
end
This will set up the database and the many-to-many models connections. This will get you started. Now you have to go create some views

Problem with has_many :through Association in Ruby on Rails

I've got a problem with a has_many :through association, i cant call the u1.UsersProfileAttributes.find_by_ProfileAttribute_name("icq") method, rails means that this method doesn't exist. The method u1.UsersProfileAttributes.find_by_ProfileAttribute_id(3) works correctly. u1 is a user object. I don't know whats the problem, because my associations seems to be okay. Have a look:
class ProfileAttribute < ActiveRecord::Base
has_many :UsersProfileAttributes
has_many :users, :through => :UsersProfileAttributes
end
class User < ActiveRecord::Base
has_many :UsersProfileAttributes
has_many :ProfileAttributes, :through => :UsersProfileAttributes
end
class UsersProfileAttribute < ActiveRecord::Base
belongs_to :user
belongs_to :ProfileAttribute
end
My Migration file:
class CreateProfileAttributes < ActiveRecord::Migration
def self.up
create_table :profile_attributes do |t|
t.string :name
t.integer :profile_group_id
t.timestamps
end
create_table :users_profile_attributes do |t|
t.integer :user_id
t.integer :ProfileAttribute_id
t.string :value
end
end
def self.down
drop_table :profile_attributes
drop_table :users_profile_attributes
end
end
You need to query ProfileAttributes, not UsersProfileAttributes. This should work for you: u1.ProfileAttributes.find_by_name('icq')
u1.UsersProfileAttributes.find_by_ProfileAttribute_id(3) works because ProfileAttribute_id is an attribute of UsersProfileAttribute ... so ActiveRecord builds you a dynamic finder.
u1.UsersProfileAttributes.find_by_ProfileAttribute_name("icq") does not work because ProfileAttribute_name is not an attribute of UsersProfileAttribute.

Resources