Rails 5—How to add UUID column - mysql

There doesn't seem to be much documentation on UUIDs in Rails 5. All I've found is this code:
create_table :users, id: :uuid do |t|
t.string :name
end
That works great if you're creating a table, but what if you're updating an already-existing table?
How do you add a UUID column to a table?

To migrate from default id to use uuid, try writing migration like this:
class ChangeProjectsPrimaryKey < ActiveRecord::Migration
def change
add_column :projects, :uuid, :uuid, default: "uuid_generate_v4()", null: false
change_table :projects do |t|
t.remove :id
t.rename :uuid, :id
end
execute "ALTER TABLE projects ADD PRIMARY KEY (id);"
end
end

Here's how to add a uuid column to an existing Rails table.
class AddUuidToContacts < ActiveRecord::Migration[5.1]
def change
enable_extension 'uuid-ossp' # => http://theworkaround.com/2015/06/12/using-uuids-in-rails.html#postgresql
add_column :contacts, :uuid, :uuid, default: "uuid_generate_v4()", null: false
execute "ALTER TABLE contacts ADD PRIMARY KEY (uuid);"
end
end
If you forget to add enable_extension 'uuid-ossp', you'll get these errors:
PG::UndefinedFunction: ERROR: function uuid_generate_v4() does not
exist ActiveRecord::StatementInvalid: PG::UndefinedFunction: ERROR:
function uuid_generate_v4() does not exist

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.

Rails 5.2 migrations conundrum, create primary key and foreign keys

I cannot figure out what is wrong with my migrations, but they won't work, trying to add foreign key on products from primary key in categories
20180724203015_create_categories.rb
class CreateCategories < ActiveRecord::Migration[5.2]
def change
create_table :categories, id: false do |t|
t.string :name
t.text :description
t.integer :category_id, primary_key: true, auto_increment: true
t.timestamps
end
end
end
20180724203105_create_products.rb
class CreateProducts < ActiveRecord::Migration[5.2]
def change
create_table :products do |t|
t.string :name
t.references :category_id, foreign_key: true, index: true
t.timestamps
end
end
end
however this always ends with an error, cannot add foreign key contraint
Ruby on Rails embraces the convention over configuration principle. That means if you follow the Rails conventions things will be much easier and almost no configuration is needed.
In your migrations you decided to fight one of the Rails conventions: The primary key of a database is stored in an id column. When you decide to not follow this convention then you cannot simply use the references method anymore without telling Rails how you set up your database instead.
My advice is: Unless you have a very, very good reason, do not fight against Rails conventions. It will not only make this task (database migration) more complex, but it also increased the risk that something else needs more configuration (ActiveRecord models for example) or that things might break in gems or when you update Rails to a newer version.
That said: Change your migration to use the Rails conventions:
# 20180724203015_create_categories.rb
class CreateCategories < ActiveRecord::Migration[5.2]
def change
create_table :categories do |t| # <- create an `id` column per default
t.string :name
t.text :description
t.timestamps
end
end
end
# 20180724203105_create_products.rb
class CreateProducts < ActiveRecord::Migration[5.2]
def change
create_table :products do |t|
t.references :category, foreign_key: true # <- just name the relation
t.string :name
t.timestamps
end
end
end
The answer to this is to remove t.integer :category_id, and rely on rails' :id column for each table, references can be made with #category or with #product, and as mentioned in the comments - #category = Cateogory.find(params[:id]) or Category.find(params[:category_id]) as I believe that is how it is referenced in routes. Thank you.

Rails with mysql cannot create json type column

I have created Rails(3.2) application with mysql(5.7.16) backend. I can't add json type column when creating table but I can able to add json column by new migration. I have used following code in create table migration, What was wrong here ?
class CreateShoppingCartItemSpecialInfos < ActiveRecord::Migration
def change
create_table :shopping_cart_item_special_infos do |t|
t.integer :shopping_cart_checkout_option_id
t.json :special_info
t.timestamps
end
end
end
You should be able to create JSON column type. Probably you run in couple of errors. In case there is a error after migration try first reverting it:
rake db:migrate:down VERSION=<version>
Than you can try like this:
class CreateShoppingCartItemSpecialInfos < ActiveRecord::Migration
def change
create_table :shopping_cart_item_special_infos do |t|
t.integer :shopping_cart_checkout_option_id
t.column :special_info, :json
t.timestamps
end
end
end

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.

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

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 !