Refactoring composite primary key to simple key for Rails? - sql

My application uses Ruby on Rails ActiveRecord models, which don't allow for composite keys without installing a third-party gem such as composite-primary-keys. Is there a way I can refactor a composite key into a simple key so it will fit this paradigm, or should I bite the bullet and install the gem?
I'm still at the early design stage so I have no data I need to worry about, and I'd like to stay as true to Rails idioms as possible.
I'm creating a recipe database that can list ingredients and instructions in a step-by-step manner. The database schema is similar to the one shown below, and is using composite keys in the Recipe_Steps and Recipe_Step_Ingredients tables (bottom center of image).

You're very likely overthinking and overcomplicating the problem. Stick with the AR idiom of using a single primary key named id.
For join tables use foreign keys instead of compound PKs. Also stick to the naming conventions unless you want to look inept or annoy other devs by violating the principle of least surprise. That means:
use snake_case for everything (table names, columns, index names etc.)
don't prefix the column with the table name. It just makes every variable in your application longer and is not needed in a ORM.
use _id for foreign key columns. ex; parent_id
use _at for timestamps. ex; confirmed_at
use thing_other_things for join tables unless there is a more descriptive name
Also many of these cases should just use an indirect relation to join up the hierarchy instead of duplicating foreign keys.
This is an example DB schema:
ActiveRecord::Schema.define(version: 20161214013752) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "ingredient_types", force: :cascade do |t|
t.string "name"
t.string "description"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "ingredients", force: :cascade do |t|
t.integer "ingredient_type_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["ingredient_type_id"], name: "index_ingredients_on_ingredient_type_id", using: :btree
end
create_table "recipe_ingredients", force: :cascade do |t|
t.integer "recipe_id"
t.integer "ingredient_id"
t.float "quantity"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["ingredient_id"], name: "index_recipe_ingredients_on_ingredient_id", using: :btree
t.index ["recipe_id"], name: "index_recipe_ingredients_on_recipe_id", using: :btree
end
create_table "steps", force: :cascade do |t|
t.integer "recipe_id"
t.integer "ordinal"
t.text "instruction"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["recipe_id"], name: "index_steps_on_recipe_id", using: :btree
end
create_table "recipes", force: :cascade do |t|
t.string "name"
t.string "description"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_foreign_key "ingredients", "ingredient_types"
add_foreign_key "recipe_ingredients", "ingredients"
add_foreign_key "recipe_ingredients", "recipes"
add_foreign_key "steps", "recipes"
end
class IngredientType < ApplicationRecord
has_many :ingredients
end
class Ingredient < ApplicationRecord
belongs_to :ingredient_type
has_many :recipe_ingredients
has_many :recipes, through: :recipe_ingredients
end
class RecipeIngredient < ApplicationRecord
belongs_to :recipe
belongs_to :ingredient
has_one :ingredient_type, through: :ingredient
end
class Step < ApplicationRecord
belongs_to :recipe
end
class Recipe < ApplicationRecord
has_many :recipe_ingredients
has_many :ingredients, through: :recipe_ingredients
has_many :steps
end

Related

How to simplify my Active Record code (I want to filter has_many using join table column)?

This is my schema
create_table "duties", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.boolean "is_general", default: false
t.boolean "write_all", default: true
end
create_table "duties_users", force: :cascade do |t|
t.bigint "duty_id"
t.bigint "user_id"
t.boolean "has_write_access", default: true
t.index ["duty_id"], name: "index_duties_users_on_duty_id"
t.index ["user_id"], name: "index_duties_users_on_user_id"
end
create_table "users", force: :cascade do |t|
t.string "mail"
t.text "password_digest"
t.datetime "birth_date"
t.boolean "is_admin", default: false
t.integer "rating", default: 0
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.string "name", default: ""
t.string "surname", default: ""
t.string "patronymic", default: ""
t.datetime "restore_date", default: "2021-02-11 09:57:14"
t.boolean "is_boss", default: false
end
My join model
class DutyUser < ApplicationRecord
self.table_name = "duties_users"
belongs_to :user
belongs_to :duty
end
User model
class User < ApplicationRecord
has_secure_password
has_many :duties_users, class_name: "DutyUser"
has_many :duties, through: :duties_users, class_name: "Duty"
end
And duty model
class Duty < ApplicationRecord
has_many :duties_users, class_name: "DutyUser"
has_many :users, through: :duties_users, class_name: "User"
end
I want to find all users who have write acces to a specific duty. In order to achive it i need to use something like this:
Duty.first.duties_users.find_all{|m| m.has_write_access}.map{|m| m.user}
How can i simplify this line of code?
The key points here are:
Duty has_many users. So if you have a duty, you can always do duty.users and that will give you the users in what's known as an "ActiveRecord Relation"
ActiveRecord Relations act a lot like arrays, except that they only give you access to instances of one model (in this case, duty.users will return a relation full of User instances) which allow you to run further queries, as well as any class methods on User. Note that unlike an array, a Relation doesn't actually contain anything. It quietly builds some SQL, and only sends that SQL to the database when you force it to (e.g. by iterating over it with .each, or asking for a .count)
Queries can be done using the where method - e.g. .where(has_write_access: true) - this is done in the database, so it's super quick.

Association command using has_many :through associations in Ruby on Rails

I have a has_many :through association.
#app/user.rb
class User < ApplicationRecord
has_many :members
has_many :projects, :through => :members
end
#app/project.rb
class Project < ApplicationRecord
has_many :members
has_many :users, :through => :members
end
#app/member.rb
class Member < ApplicationRecord
belongs_to :user
belongs_to :project
end
I have the database schema as follows:
create_table "members", force: :cascade do |t|
t.integer "user_id"
t.integer "project_id"
t.integer "is_owner"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["project_id"], name: "index_members_on_project_id"
t.index ["user_id"], name: "index_members_on_user_id"
end
create_table "projects", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "users", force: :cascade do |t|
t.string "first_name"
t.string "last_name"
t.string "email"
t.string "password_digest"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
I can get Member ID, User_ID, Project_ID, and is_owner when I use the command #project.members
I can get user_id, first_name, last_name, email, and password when I use the command #project.users
What command should I use to get member_id, first_name, last_name?
I can get what I want using the SQL query SELECT * FROM members INNER JOIN users ON users.id = members.user_id but I don't want to use raw SQL.
Can someone tell me how to convert that query into a Ruby on rails command?
You can get your desired result using following code
Member.joins(:user)
It will generate the same query what you are specifying in your question i.e.
SELECT * FROM members INNER JOIN users ON users.id = members.user_id

How to Sort By Association Attribute Value in Rails

I'm sure I'm missing something basic, but I have a model User which belongs_to a Company model which has a name attribute.
How can I sort all users by their Company name?
#users = User.all #sort this collection in order of the associated company's name
class User < ActiveRecord::Base
belongs_to :company
end
class Company < ActiveRecord::Base
end
create_table "users", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "company_id"
end
create_table "companies", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
User.includes(:company).order('company.name') . I believe ASC is default, but you can add it if necessary. You may also have to specify the Company table name in the model, or change the table name to 'companies', as that is the default pluralization convention over configuration will assume I believe.

Moving data to another table

I have a question associated with moving data from one table to another. I am using Postgres as a database.
I have two models:
class User < ActiveRecord::Base
has_many :emails
end
class Email < ActiveRecord::Base
belongs_to :user
end
schema looks like this:
create_table "users", force: :cascade do |t|
t.string "first_name", limit: 255
t.string "last_name", limit: 255
t.string "email"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "emails", force: :cascade do |t|
t.string "email", null: false
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
Now I want to move the latest email for the user and save it in users table email column. I can easily do it using Rails models in migration but then when I rename model or remove it. Migration will fail. Is there any easy way to do it with raw sql?
You can use window functions. The example below is a starting point.
UPDATE users u
SET u.email = (SELECT
MAX(FIRST_VALUE(email)) OVER (PARTITION BY user_id ORDER BY created_at DESC)
FROM emails e
WHERE e.user_id = u.id);
You can still use ActiveRecord with fake classes just for the purpose of migration. Something like:
class YourMigration < ActiveRecord::Migration
class FakeUser < ActiveRecord::Base
self.table_name = 'users'
has_many :fake_emails, foreign_key: 'user_id'
end
class FakeEmail < ActiveRecord::Base
self.table_name = 'emails'
belongs_to :fake_user, foreign_key: 'user_id'
end
def change
FakeUser.all.each do |user|
[...]
end
end
end

rails no such column error

I have the following app. A Movie has many reviews, a moviegoer has many reviews.
When I try to associate a review with a movie I get the following error
Review Load (0.1ms) SELECT "reviews".* FROM "reviews" WHERE "reviews"."movie_id" = 5
SQLite3::SQLException: no such column: reviews.movie_id: SELECT "reviews".* FROM "reviews" WHERE "reviews"."movie_id" = 5
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: reviews.movie_id: SELECT "reviews".* FROM "reviews" WHERE "reviews"."movie_id" = 5
after using a sql gui editor I found that the correct query should be
SELECT "reviews".* FROM "reviews" WHERE "movie_id" = 5
review.rb
class Review < ActiveRecord::Base
belongs_to :movie
belongs_to :moviegoer
attr_protected :moviegoer_id
end
movie.rb and moviegoer.rb have
has_many :reviews
in them.
schema.rb
ActiveRecord::Schema.define(:version => 20130222225620) do
create_table "moviegoers", :force => true do |t|
t.string "name"
t.string "provider"
t.string "uid"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
create_table "movies", :force => true do |t|
t.string "title"
t.string "rating"
t.text "description"
t.datetime "release_date"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
create_table "reviews", :force => true do |t|
t.integer "potatoes"
t.text "comments"
t.integer "moviegoers_id"
t.integer "movies_id"
end
end
What am I doing wrong? why is rails querying "reviews"."movie_id" instead of just "movie_id"?
You have the wrong column name in your migration. The rails convention is that foreign keys are to be singular. If they are not then you need to tell rails what the foreign key is with an options hash on the association.
Either rollback your migration, fix the column name (moviegoers_id is wrong as well) then migrate again, or tell rails the foreign key.
Class Review < ActiveRecord::Base
belongs_to :movie, :foreign_key => 'movies_id'
belongs_to :moviegoer, :foreign_key => 'moviegoers_id'
end
And the same has to happen on the has many side of both models.