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

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.

Related

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.

How to write the active record query

I have three models in context of this question:
class ClearanceBatch < ActiveRecord::Base
has_many :items
belongs_to :user
end
class Item < ActiveRecord::Base
belongs_to :style
belongs_to :user
belongs_to :clearance_batch
validates :id, :uniqueness => true
end
class User < ActiveRecord::Base
has_many :items, dependent: :destroy
has_many :clearance_batches, dependent: :destroy
enum role: {staff: 0, vendor: 1, admin: 2}
end
Schema:
create_table "clearance_batches", force: :cascade do |t|
t.datetime "created_at"
t.datetime "updated_at"
t.boolean "status", default: false
t.string "boughtby", default: ""
t.integer "user_id"
end
add_index "clearance_batches", ["user_id"], name: "index_clearance_batches_on_user_id"
create_table "items", force: :cascade do |t|
t.string "size"
t.string "color"
t.string "status"
t.decimal "price_sold"
t.datetime "sold_at"
t.integer "style_id"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "clearance_batch_id"
t.integer "user_id"
end
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "role", default: 0
end
I want to find all items in a batch of the currently logged in user(mainly vendor) with the status "clearanced", and get their details in a loop from controller to my view
Can anyone please help me out with the active record query? Please! :)
The SQLite Query I think would be:
Select I.id from clearance_batches C INNER JOINS Items I on C.id = I.clearance_batch_id where C.user_id = "1" and I.status = "clearanced"
(If 1 is the current user, keeping in mind I am only allowing user of role vendors to be a user in clearance_batch table)
(1) Query:
Items.where(status: "clearanced")
.joins(:clearance_batches)
.where(clearance_batches: {user_id: current_user})
(2) Controller:
#clearanced_items = query(1)
(3) View:
<% #clearanced_items.each do |c_item| %>
...
<% end %>

Refactoring composite primary key to simple key for Rails?

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

How do I write an ActiveRecord query that filters out results from a joined table?

Users and Sessions are joined by a has_and_belongs_to_many association.
How do I get the unique list of Users that meet the following conditions?
user.coach == true
user.available == true
And then NOT include a user if that user is a coach in any active Session:
session.coach_id == user.id
session.call_ends_at == nil
Is there a way I can write this with ActiveRecord Query language? Do I need to write a pure SQL statement? Some kind of hybrid? What would YOU do?
I also have scopes defined that could be helpful here. But I'm not sure how to add them in:
User.available_coaches (scope)
Session.in_progress (scope)
User model
class User < ActiveRecord::Base
has_many :client_sessions, class_name: 'Session', foreign_key: :client_id
has_many :coach_sessions, class_name: 'Session', foreign_key: :coach_id
scope :coaches, -> { where(coach: true) }
scope :available_coaches, -> { coaches.where(available: true) }
Session model
class Session < ActiveRecord::Base
belongs_to :client, class_name: 'User'
belongs_to :coach, class_name: 'User'
scope :in_progress, -> { where.not(coach: nil).where(call_ends_at: nil) }
Schema
create_table "sessions", force: :cascade do |t|
t.integer "client_id"
t.integer "coach_id"
t.boolean "canceled", default: false
t.datetime "coach_accepted_at"
t.datetime "call_begins_at"
t.datetime "call_ends_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "sessions", ["client_id"], name: "index_sessions_on_client_id", using: :btree
add_index "sessions", ["coach_id"], name: "index_sessions_on_coach_id", using: :btree
create_table "users", force: :cascade do |t|
t.string "first_name"
t.string "last_name"
t.boolean "coach", default: false
t.boolean "available", default: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
I would do it with SQL exists:
User.where(coach: true, available: true).
where("not exists (select 1 from sessions " +
"where sessions.coach_id = users.id and sessions.call_ends_at is null)")
Note that since there is no join to sessions there is no need for .uniq.