Rails active record object save action fails because of a malformed query - mysql

I have a problem in Rails in the part of my app that handles landing pages for registration emails. When a user is invited to the app, an Invitation active record instance is created, and user is sent an email with a link containing the id of the invitation (a random string token).
Then, when the link is clicked, I store that token in session, and at one point in the service layer, I update it's 'status' attribute to 'clicked', something like this:
#invitation = Invitation.find_by_id(session[:registration][:invitation_token])
unless #invitation.blank?
session[:registration][:account_details]['referer'] = #invitation.promoter.username
unless #invitation.status == APP_CONFIG['invitation_status']['clicked']
#invitation.status = APP_CONFIG['invitation_status']['clicked']
#invitation.save
end
end
Upon executing the #invitation.save line, I get an active record error:
!! #<ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'invitations.' in 'where clause': UPDATE `invitations` SET `status` = 'clicked', `updated_at` = '2015-11-11 11:07:24' WHERE `invitations`.`` = 'fd05ee5a-e790-48cc-9e7e-d30d3e88919b'>
The Invitation's id column name seems to be ommited from the query for some reason, and I can't figure out why. Not sure what is wrong or what to do.
Invitation migration:
class CreateInvitations < ActiveRecord::Migration
def change
create_table :invitations, id: false do |t|
t.string :id, limit: 36, :auto_increment => false
t.string :promoter_id
t.string :email
t.string :status
t.timestamps
end
end
end
Invitation model:
class Invitation < ActiveRecord::Base
#region Callbacks
before_create :set_uid
#endregion
belongs_to :promoter, class_name: 'User', foreign_key: 'promoter_id'
private
def set_uid
self.id = SecureRandom.uuid unless self.id
end
end

You should mark the id column as the primary_key:
create_table :invitations, id: false do |t|
t.string :id, limit: 36, primary_key: true, auto_increment: false
...
I think that in your case the best way to solve this problem is to drop the table and create it again with correct migration. But if there is data that you don't want to loose, you can also try to do it like this:
class MarkIdAsPrimaryKey < ActiveRecord::Migration
def change
change_column :invitations, :id, :string, limit: 36, primary_key: true, auto_increment: false
end
end
Keep in mind that this migration is not reversible, it remains under your responsibility if you will use it ) Good luck !

Related

Why isn't Ruby on Rails loading my associated objects with .includes()?

My Problem
I'm attempting to retrieve data through a foreign key association in my Ruby on Rails application. The data from the primary table is loaded correctly, but the associated objects are not being loaded and are always nil.
Background Info (Migrations, Database tables, and Model classes)
I'm currently working with two tables:
eval_forms
user_details
The tables are created through Rails migrations.
The user_details table is created through a single migration:
class CreateUserDetails < ActiveRecord::Migration[5.2]
def change
create_table :user_details do |t|
t.string :eduPersonPrincipalName, unique: true
t.string :DisplayName, default: 'NULL'
t.string :Email, default: 'NULL'
t.string :Role, default: 'Student'
t.boolean :hasAppointment, default: '0'
t.timestamps
end
end
def self.down
drop_table :user_details
end
end
and the eval_forms table has had a few migrations to create and update it:
class CreateEvalForms < ActiveRecord::Migration[6.1]
def change
create_table :eval_forms do |t|
t.belongs_to :form_builder, foreign_key: 'form_builder_id'
t.belongs_to :course, foreign_key: 'course_id'
t.string :Description
t.datetime :OpenDate
t.datetime :CloseDate
t.timestamps
end
end
end
class UpdateEvalForms < ActiveRecord::Migration[6.1]
def change
add_column :eval_forms, "Author_user_details_id", :bigint, null: false
add_foreign_key :eval_forms, :user_details, column: "Author_user_details_id"
add_column :eval_forms, "Year", :integer
add_column :eval_forms, "Semester", :string
add_column :eval_forms, "IsArchived", :boolean
end
end
I know that the foreign key is set up correctly as it is listed correctly in MySQL. Here's a reference from MySQL of the 2 tables and their relation:
Additionally, I've set up the model classes in my Rails app.
eval_form:
class EvalForm < ApplicationRecord
has_many :eval_forms_roles
has_many :roles, through: :eval_forms_roles
has_many :eval_forms_courses
has_many :courses, through: :eval_forms_courses
has_many :eval_responses
has_many :eval_reminders
belongs_to :user_detail
validates :formName, presence: true
validates :formData, presence: true
end
user_detail:
class UserDetail < ApplicationRecord
has_one :la_detail
has_many :eval_responses
has_many :eval_forms
end
So What's Wrong?
Lastly, here is the code to retrieve the objects from the database and the section where I'm getting my error.
My controller action:
def index
# list *all* existing evaluation forms, with options to filter by OpenDate, CloseDate, etc (todo)
#EvalForms = EvalForm.includes(:user_detail)
end
My view:
<td><%= ef.user_detail.DisplayName %></td>
My error:
NoMethodError in Evaluations::EvalForms#index
undefined method `DisplayName' for nil:NilClass
Extracted Source location: <td><%= ef.user_detail.DisplayName %></td>
Restating the problem
In conclusion, I'm really confused as to why the associated user_detail objects are not being retrieved despite my .includes() statement in the controller action. I'm pretty new to Ruby as well as Rails, but there are other sections in the application that look similar to this and work correctly so I don't see what my issue is.
I would start by using conventional naming which in Rails means snake_case everywhere:
class CreateUserDetails < ActiveRecord::Migration[5.2]
def change
create_table :user_details do |t|
t.string :edu_person_principal_name, unique: true
t.string :display_name
t.string :email
t.string :role, default: 'Student'
t.boolean :has_appointment, default: false # let the driver handle conversion
t.timestamps
end
end
end
class UpdateEvalForms < ActiveRecord::Migration[6.1]
def change
change_table :eval_forms do |t|
t.belongs_to :author_user_details, foreign_key: { to_table: :user_details }
t.integer :year # consider using `YEAR(4)` instead
t.string :semester
t.boolean :is_archived, default: false
end
end
end
If you continue using a strange mix of camelCase and PascalCase you will need to explicitly configure all your associations and you will lose all the advantages of convention over configuration. I would not recommend this at all unless you're stuck with a legacy database as its a surefire recipe for developer confusion and bugs.
You will also get a missing constant error if you call the PascalCase methods without an explicit recipient (self):
class EvalForm < ApplicationRecord
def short_description
# uninitialized constant Description (NameError)
Description.truncate(27, separator: ' ')
end
end
While you can fix this with self.Description.truncate(27, separator: ' ') its still very smelly.
In this case if you want to call the column author_user_details_id instead of user_details_id which is derived from the name you need to configure the assocation to use the non-conventional name:
class EvalForm < ApplicationRecord
belongs_to :user_detail, foreign_key: :author_user_details_id
end
class UserDetail < ApplicationRecord
has_many :eval_forms, foreign_key: :author_user_details_id
end
If the rest of your schema looks like this you'll have to do this across the board.

Migration - How to add a UNIQUE constraint to an already populated table?

I have a technicians table which consists only of name and timestamps columns in production, but now I need to use it as a devise model.
class Technician < ActiveRecord::Base
validates_presence_of :name
validates_uniqueness_of :name
end
Using 'rails generate devise technician' led me to a migration with all that is necessary to make my technician model a devise model.
class AddDeviseToTechnicians < ActiveRecord::Migration
def change
change_table(:technicians) do |t|
## Database authenticatable
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""
end
end
end
But I can't migrate since email is a 'unique: true' column, nor drop my technicians table. What should I do?
I was thinking of setting the email field of each technician as: name_attribute + "#email.com", but what's the best way to do this?
I'm using a MYSQL database.

How to build M to M relationships between two data model

Condition:
Tutor can follow or dis-follow student
Student can follow or dis-follow tutor
Tutor and Student are two different data models.
I build a middle data model calledapplication connecting both model
Application model:
class CreateApplications < ActiveRecord::Migration
def change
create_table :applications do |t|
t.belongs_to :tutor, index: true
t.belongs_to :student, index: true
t.timestamps null: false
end
end
end
application.rb
class Application < ActiveRecord::Base
belongs_to :tutor
belongs_to :student
end
student.rb
has_many :applications, :dependent => :destroy
has_many :tutors, through: :applications
tutor.rb
has_many :applications, :dependent => :destroy
has_many :students, through: :applications
schema.rb
ActiveRecord::Schema.define(version: 20170421093747) do
create_table "applications", force: :cascade do |t|
t.integer "tutor_id"
t.integer "student_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "applications", ["student_id"], name: "index_applications_on_student_id"
add_index "applications", ["tutor_id"], name: "index_applications_on_tutor_id".......
show.html.erb
# -->show who is the follower & person who followed & How many of them
# Error :undefined method `student' for nil:NilClass
<%= #application.student.count %> #undefined method `student' for nil:NilClass
<%= #application.tutor.count % >#undefined method `student' for nil:NilClass
<%= #application.student%> #undefined method `student' for nil:NilClass
In your controller, you need to set the #application variable.
def show
#application = Application.find(params[:id])
end
I would strongly advise against naming this class Application. The word "Application" has special meaning in Rails and you will almost certainly run into conflicts. Same goes for ApplicationController.
When model has many relationship with other model you want to use plural name, i.e. tutor.students or student.tutors
UPD: Also, you didn't pass variable into view properly, check your controller.
UPD1: Also, it's meaningless to use count on application record (btw you want to rename it to something like Courses, Classes or just StudentTutor because Application.rb is the model all your models inherit from by default, same goes to ApplicationController). It always has one student and one tutor (it belongs to both models). If you want to count related records you want to find student (if you want to count his tutors) or tutor (for vise-versa)

Rails rollback, change type and migrate again

I have a requests table;
class CreateRequests < ActiveRecord::Migration
def change
create_table :requests do |t|
t.string :from
t.string :to
t.timestamps null: false
end
end
end
I would like to rollback the database with rake db:rollback STEP = 5
destroy the Request model and create request table with;
t.datetime :from
t.datetime :to
However, I have a migration table to Request model on STEP = 2,
class AddStatusToRequest < ActiveRecord::Migration
def change
add_column :requests, :status, :string, :default => "Pending"
end
end
The problem is, if I destroy Request table and create new Request table with datetime types it creates after STEP = 2 and when I rake db:migrate rails does not add Status column to Request table. How can I overcome this?
You can create a new migration to change the column type using change_column:
command line: rails g migration change_request_to_from_column_types'
new migration:
class ChangeRequestToFromColumnTypes < ActiveRecord::Migration
def change
change_column :requests, :from, :datetime
change_column :requests, :to, :datetime
end
end
It's best not to retroactively change migrations after they've been applied to avoid state conflicts like the one you have. Perhaps the simplest solution is to create a new migration to add the types to the requests table using a guard to do nothing if the column already exists:
class AddStatusToRequestIfNotExists < ActiveRecord::Migration
def change
unless column_exists? :requests, :status
add_column :requests, :status, :string, :default => "Pending"
end
end
end
EDIT
You'll also want to make sure that you can run migrations from scratch, so you might want to update your existing migration to guard against a missing table as follows:
class AddStatusToRequest < ActiveRecord::Migration
def change
unless table_exists? :requests
add_column :requests, :status, :string, :default => "Pending"
end
end
end
Not ideal, but it's probably the safest approach.

How to Show Table Entry Name Instead of id on Show or Index

Ok. Asked this question yesterday and since started a whole new rails app to see if starting from scratch helps.
Here's how this app works. A user will create a new countertop and enter their zip code, the countertop size and the type of countertop. Possible countertop types are stored in a model called "Countertype". Users select the countertype through a collection_select method, which lists all the entries in the Countertype table. The responses to this Countertop form are saved in a Countertop table, which has a "countertop_id" integer column.
When the user lands on the Show and then the Index page, I'd like the name of the countertype to be visible instead of the integer.
How do I do this? It's killing me.
Here's my schema:
create_table "countertops", force: :cascade do |t|
t.string "counterzip"
t.string "countersize"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "countertype_id"
end
create_table "countertypes", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
Here's my index and show
def index
#countertops = Countertop.all
#countertops = Countertop.includes(:countertype).all
end
def show
#countertops = Countertop.all
#countertops = Countertop.includes(:countertype).all
end
Countertop.rb:
class Countertop < ActiveRecord::Base
has_one :countertype
end
Countertype.rb
class Countertype < ActiveRecord::Base
belongs_to :countertop
end
Show:
<p>
<strong>Counter Type:</strong>
<%= #countertop.countertype.name %>
</p>
Index:
<% #countertops.each do |countertop| %>
<tr>
<td><%= countertop.counterzip %></td>
<td><%= countertop.countertype.name %>
Here's a readout from my console for both tables.
Countertop.last
Countertop Load (0.2ms) SELECT "countertops".* FROM "countertops" ORDER BY "countertops"."id" DESC LIMIT 1
=> #<Countertop id: 1, counterzip: "19111", countersize: "100", created_at: "2015-10-01 20:44:29", updated_at: "2015-10-01 20:44:29", Countertype_Id: 1>
2.2.1 :029 >
Countertype.last
Countertype Load (0.7ms) SELECT "countertypes".* FROM "countertypes" ORDER BY "countertypes"."id" DESC LIMIT 1
=> #<Countertype id: 1, name: "Granite", created_at: "2015-10-01 20:15:12", updated_at: "2015-10-01 20:15:12">
Heres's the error message:
SQLite3::SQLException: no such column: countertypes.countertop_id: SELECT "countertypes".* FROM "countertypes" WHERE "countertypes"."countertop_id" = ? LIMIT 1
Changing the show to <%= #countertops.countertype_id %> displays a "1".
What do I need to fix to have it display "Granite" instead of "1" ??
Thanks!!
It looks like you have your associations backwards in the two models. belongs_to should be listed in the model whose table contains the association column (countertype_id in this case). has_one relies on the associated model to have this column.
A couple additional observations:
According to the output from Countertop.last, Countertype_Id has uppercase characters. This might not cause immediate problems with sqlite, but it may be worth changing to avoid any future problems, as Rails will assume column names are always downcased unless explicitly told otherwise.
Your #show controller method has #countertops defined (plural), but the show view uses just #countertop.