Rails: querying records by stored GlobalID string - ruby-on-rails

I have a Rails (5.2.2) app (Postgres database) with some Models related to different geographies:
- districts (have many sectors)
-- sectors (have many cells, have one district)
--- cells (have many villages, have one sector)
---- villages (have many facilities, have one cell)
----- facilities (have one village)
I also have a Report Model, which, for context, records the quantity of specific technology distributed in a specific location.
#<Report id: nil, date: nil, technology_id: nil, user_id: nil, contract_id: nil, model_gid: nil, distributed: nil, checked: nil, created_at: nil, updated_at: nil, people: nil, households: nil>
This location can be any of the geography models. So I'm using GlobalID stored as a string in model_gid on the Report record.
e.g.:
#<Report id: 1, ... model_gid: "gid://liters-tracker/Village/64", ...>
Then I wrote some scopes that work fine:
scope :only_districts, -> { where('model_gid ILIKE ?', '%/District/%') }
scope :only_sectors, -> { where('model_gid ILIKE ?', '%/Sector/%') }
scope :only_cells, -> { where('model_gid ILIKE ?', '%/Cell/%') }
scope :only_villages, -> { where('model_gid ILIKE ?', '%/Village/%') }
scope :only_facilities, -> { where('model_gid ILIKE ?', '%/Facility/%') }
I thought this was a good approach because my report.model method works:
def model
GlobalID::Locator.locate model_gid
end
e.g.:
2.4.5 :001 > Report.first.model
Report Load (0.5ms) SELECT "reports".* FROM "reports" ORDER BY "reports"."id" ASC LIMIT $1 [["LIMIT", 1]]
Village Load (0.4ms) SELECT "villages".* FROM "villages" WHERE "villages"."id" = $1 LIMIT $2 [["id", 64], ["LIMIT", 1]]
=> #<Village id: 64, name: "Ruhanga", cell_id: 11, gis_id: 13080406, latitude: -2.00828333333333, longitude: 30.1708, population: 518, households: 179, created_at: "2019-01-21 22:53:06", updated_at: "2019-01-21 22:53:06">
I opted to do this string field instead of a polymorphic association because GlobalID::Locator methods can accept strings and parse out the model and ID from it. So why hassle with the association? Maybe this is the fundamental flaw in my thinking?
Because finding records based upon the model_gid seems to fail:
2.4.5 :045 > Report.all.where(model_gid: Report.first.model_gid)
Report Load (0.4ms) SELECT "reports".* FROM "reports" ORDER BY "reports"."id" ASC LIMIT $1 [["LIMIT", 1]]
Report Load (0.5ms) SELECT "reports".* FROM "reports" WHERE "reports"."model_gid" = $1 LIMIT $2 [["model_gid", "--- gid://liters-tracker/Village/64\n"], ["LIMIT", 11]]
=> #<ActiveRecord::Relation []>
(honestly not sure why the SQL is morphed into "--- gid://liters-tracker/Village/64\n" and if this is actually my problem)
2.4.5 :046 > Report.all.where("model_gid ILIKE ?", Report.first.model_gid)
Report Load (0.5ms) SELECT "reports".* FROM "reports" ORDER BY "reports"."id" ASC LIMIT $1 [["LIMIT", 1]]
Report Load (3.2ms) SELECT "reports".* FROM "reports" WHERE (model_gid ILIKE 'gid://liters-tracker/Village/64') LIMIT $1 [["LIMIT", 11]]
=> #<ActiveRecord::Relation []>
2.4.5 :049 > Report.all.where("model_gid = ?", Report.first.model_gid)
Report Load (0.3ms) SELECT "reports".* FROM "reports" ORDER BY "reports"."id" ASC LIMIT $1 [["LIMIT", 1]]
Report Load (0.6ms) SELECT "reports".* FROM "reports" WHERE (model_gid = 'gid://liters-tracker/Village/64') LIMIT $1 [["LIMIT", 11]]
=> #<ActiveRecord::Relation []>
I'm trying to get this method to work:
def self.related_to(record)
where(model_gid: record.to_global_id.to_s)
end
And I really don't understand why it's not working:
2.4.5 :010 > Report.first.model_gid
Report Load (0.6ms) SELECT "reports".* FROM "reports" ORDER BY "reports"."id" ASC LIMIT $1 [["LIMIT", 1]]
=> "gid://liters-tracker/Village/64"
2.4.5 :011 > Village.find(64).to_global_id.to_s
Village Load (0.5ms) SELECT "villages".* FROM "villages" WHERE "villages"."id" = $1 LIMIT $2 [["id", 64], ["LIMIT", 1]]
=> "gid://liters-tracker/Village/64"
2.4.5 :012 > Report.first.model_gid == Village.find(64).to_global_id.to_s
Report Load (0.4ms) SELECT "reports".* FROM "reports" ORDER BY "reports"."id" ASC LIMIT $1 [["LIMIT", 1]]
Village Load (0.3ms) SELECT "villages".* FROM "villages" WHERE "villages"."id" = $1 LIMIT $2 [["id", 64], ["LIMIT", 1]]
=> true
2.4.5 :013 > Report.all.where(model_gid: Village.find(64).to_global_id.to_s)
Village Load (0.4ms) SELECT "villages".* FROM "villages" WHERE "villages"."id" = $1 LIMIT $2 [["id", 64], ["LIMIT", 1]]
Report Load (0.4ms) SELECT "reports".* FROM "reports" WHERE "reports"."model_gid" = $1 LIMIT $2 [["model_gid", "--- gid://liters-tracker/Village/64\n"], ["LIMIT", 11]]
=> #<ActiveRecord::Relation []>
If I mimic the scopes, it does work:
def self.related_to(record)
where('model_gid ILIKE ?', "%#{record.to_global_id.to_s}%")
end
But, in the example records I've been showing, this would match Village #64 and Village #640, so it's not a good solution.
UPDATE
I thought maybe the special characters were throwing things off. But things work as expected when I use another string column on another Model:
2.4.5 :052 > Village.first.update(name: "gid://liters-tracker/Village/64")
Village Load (0.5ms) SELECT "villages".* FROM "villages" ORDER BY "villages"."id" ASC LIMIT $1 [["LIMIT", 1]]
(0.2ms) BEGIN
Cell Load (0.2ms) SELECT "cells".* FROM "cells" WHERE "cells"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
Village Exists (0.3ms) SELECT 1 AS one FROM "villages" WHERE "villages"."gis_id" = $1 AND "villages"."id" != $2 LIMIT $3 [["gis_id", 11070101], ["id", 1], ["LIMIT", 1]]
Village Update (0.3ms) UPDATE "villages" SET "name" = $1, "updated_at" = $2 WHERE "villages"."id" = $3 [["name", "gid://liters-tracker/Village/64"], ["updated_at", "2019-07-06 22:16:38.585563"], ["id", 1]]
(1.2ms) COMMIT
=> true
2.4.5 :053 > Village.where(name: "gid://liters-tracker/Village/64")
Village Load (0.3ms) SELECT "villages".* FROM "villages" WHERE "villages"."name" = $1 LIMIT $2 [["name", "gid://liters-tracker/Village/64"], ["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Village id: 1, name: "gid://liters-tracker/Village/64", cell_id: 1, gis_id: 11070101, latitude: -2.054922, longitude: 30.0912883, population: 513, households: 110, created_at: "2019-01-21 22:53:04", updated_at: "2019-07-06 22:16:38">]>
I thought maybe I needed an index on the Report.model_gid field. But it hasn't made a difference.
class AddModelGidIndexToReports < ActiveRecord::Migration[5.2]
def change
add_index :reports, :model_gid
end
end
UPDATE 2
(this is based upon my own provided 'answer', but since it's a question, I put it here)
#MichaelChaney:
Just so I'm clear, are you suggesting something like this:
class Report < ApplicationRecord
belongs_to :technology, inverse_of: :reports
belongs_to :user, inverse_of: :reports
belongs_to :contract, inverse_of: :reports
enum geography: { district: 'district', sector: 'sector', cell: 'cell', village: 'village', facility: 'facility' }
At this point, should I just add a geography_id integer column and stop using GlobalID?
What about just going polymorphic instead?

For the sake of closing the record, I switched to a polymorphic association. Probably not quite as fast as the Enum solution that #MichaelChaney suggests in the comments on the previous answer, but fast enough for my in-house app and creates an association known to my app.
class Report < ApplicationRecord
belongs_to :technology, inverse_of: :reports
belongs_to :user, inverse_of: :reports
belongs_to :contract, inverse_of: :reports
# serialize :model_gid #<-- this was real bad as #MichaelChaney points out
# enum geography: { district: 'district', sector: 'sector', cell: 'cell', village: 'village', facility: 'facility' } #<-- this is probably the fastest option
belongs_to :reportable, polymorphic: true #<-- this is probably the middle ground, as the :reportable_id and :reportable_type columns are indexed together
And this was partnered with the following on all my Geography models, e.g.:
class Facility < ApplicationRecord
belongs_to :village, inverse_of: :facilities
has_one :cell, through: :village, inverse_of: :facilities
has_one :sector, through: :cell, inverse_of: :facilities
has_one :district, through: :sector, inverse_of: :facilities
has_many :reports, as: :reportable, inverse_of: :reportable #<-- tadaa
So now I don't even need my initial method as I can compare the results of reports.reportable with the record I have to see if they are associated.
The lesson I learned: in the early stages, I need to think more about RdBMS and what associations I'll care a lot about so I don't try doing dumb Regex searches across my dB.
The other lesson: keep better notes in my code base, so when I change strategies I can correctly un-wind things I implemented.

Ugh. This is a face-palm moment.
class Report < ApplicationRecord
belongs_to :technology, inverse_of: :reports
belongs_to :user, inverse_of: :reports
belongs_to :contract, inverse_of: :reports
serialize :model_gid
scope :only_districts, -> { where('model_gid ILIKE ?', '%/District/%') }
...
Report.model_gid is serialized, which I did before I discovered GlobalID. I think I was planning to save some key-value hash like {model: 'Village', id: '64'}.
Now to figure out how to un-serialize a column.

Related

How to use an Enum in Concern

I have this (simplified) concern:
module Nobi::Personable
extend ActiveSupport::Concern
included do
belongs_to :person, :autosave => true delegate :gender,
:gender=, :gender_changed?, :gender_was, :to => :person, :allow_nil => true
enum gender: { male: "male", female: "female" }
end
end
A Resident has this concern.
Now when I do this:
2.6.6> Resident.last.gender
Resident Load (16.2ms) SELECT "residents".* FROM "residents" ORDER BY "residents"."id" DESC LIMIT $1 [["LIMIT", 1]]
Person Load (16.1ms) SELECT "people".* FROM "people" WHERE "people"."id" = $1 LIMIT $2 [["id", 48], ["LIMIT", 1]]
=> "male"
However when I ask: male? I get:
2.6.6> Resident.last.male?
Resident Load (17.0ms) SELECT "residents".* FROM "residents" ORDER BY "residents"."id" DESC LIMIT $1 [["LIMIT", 1]]
=> false
How is this possible?
If I include the enum on the Person model, it works fine:
Person.last.male?
Person Load (15.9ms) SELECT "people".* FROM "people" ORDER BY "people"."id" DESC LIMIT $1 [["LIMIT", 1]]
=> true
I've created a minimalistic demo app which demonstrates this behavior:
https://github.com/rept/enum_app
r = Resident.create(gender: :male, local_gender: :local_male)
r.local_male?
=> true
r.male?
=> false
Declare an enum attribute where the values map to integers in the
database, but can be queried by name.
-- ActiveRecord::Enum
You either need to use an integer column with your enum or declare the mapping explicitly. Rails assumes that the values stored in the database are equal to the indices of the array passed to enum. Since "male" != 0 #male? will return false.
module Nobi::Personable
extend ActiveSupport::Concern
included do
belongs_to :person, :autosave => true
enum gender: {
male: "male",
female: "female"
}
end
end
While using a string column kind of defeats the point of using an enum in the first place declaring the mapping explicitly is seen as a best practice as it will prevent hard to debug breakages that can be caused by simply reordering the enum values in the array.
And if you need proof:
class User < ApplicationRecord
enum gender: [:male, :female] # users.gender is an integer column
end
irb(main):003:0> User.first
(0.9ms) SELECT sqlite_version(*)
User Load (2.0ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<User id: 9, created_at: "2020-10-29 09:24:12", updated_at: "2020-10-29 09:24:12", gender: "male">
irb(main):004:0> User.first.male?
User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> true
class Resident < ApplicationRecord
# residents.gender is a string column
enum gender: {
male: 'male',
female: 'female'
}
end
irb(main):001:0> Resident.create!(gender: 'male')
(0.4ms) SELECT sqlite_version(*)
(0.1ms) begin transaction
Resident Create (1.6ms) INSERT INTO "residents" ("gender", "created_at", "updated_at") VALUES (?, ?, ?) [["gender", "male"], ["created_at", "2020-10-29 09:56:59.471917"], ["updated_at", "2020-10-29 09:56:59.471917"]]
(4.7ms) commit transaction
=> #<Resident id: 1, gender: "male", created_at: "2020-10-29 09:56:59", updated_at: "2020-10-29 09:56:59">
irb(main):002:0> Resident.first.male?
Resident Load (0.2ms) SELECT "residents".* FROM "residents" ORDER BY "residents"."id" ASC LIMIT ? [["LIMIT", 1]]
=> true

ActiveRecord AND query

I have 3 models : User, Conversation and ConversationUser
class User < ApplicationRecord
has_many :conversation_users, dependent: :destroy
has_many :conversations, through: :conversation_users
end
class Conversation < ApplicationRecord
has_many :conversation_users, dependent: :destroy
has_many :users, through: :conversation_users
end
class ConversationUser < ApplicationRecord
belongs_to :conversation
belongs_to :user
end
My purpose
I would like to find a Conversation when 2 users have the same conversation (a conversation must only have 2 conversation_users with the id of these 2 users)
For the test
I only created ONE conversation with TWO conversation_users
conversation = Conversation.create
conversation.conversation_users.create(user: User.first)
conversation.conversation_users.create(user: User.second)
My Problem
I want to find the conversation with JUST the first User AND second User.
If I do the following query, Active Record will still show me a conversation because it's doing a OR clause but I need a AND.
The correct result should show me an empty array because there is not a conversation with User.first AND User.third
Conversation.joins(:conversation_users).where(conversation_users: {user_id: [User.first.id, User.third.id]})
User Load (3.3ms) SELECT "users".* FROM "users" ORDER BY "users"."created_at" ASC LIMIT $1 [["LIMIT", 1]]
User Load (2.5ms) SELECT "users".* FROM "users" ORDER BY "users"."created_at" ASC LIMIT $1 OFFSET $2 [["LIMIT", 1], ["OFFSET", 2]]
Conversation Load (3.9ms) SELECT "conversations".* FROM "conversations" INNER JOIN "conversation_users" ON "conversation_users"."conversation_id" = "conversations"."id" WHERE "conversation_users"."user_id" IN ($1, $2) LIMIT $3 [["user_id", "274d2f54-2418-4dec-a696-a5f62196ee85"], ["user_id", "0ef2f797-3518-4096-951b-1837ca28e4e4"], ["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Conversation id: "35556705-a162-4ee4-9776-963ae87bcfa8", created_at: "2020-04-20 09:34:05", updated_at: "2020-04-20 09:34:05">]>
Expectations
Should find a Conversation with User.first and User.second
Should NOT find a Conversation with User.first and User.third
I tried this query for the first expectation but it is returning an empty array
Conversation.joins(:conversation_users).where(conversation_users: {user_id: User.first.id}).where(conversation_users: {user_id: User.second.id})
User Load (3.8ms) SELECT "users".* FROM "users" ORDER BY "users"."created_at" ASC LIMIT $1 [["LIMIT", 1]]
User Load (4.0ms) SELECT "users".* FROM "users" ORDER BY "users"."created_at" ASC LIMIT $1 OFFSET $2 [["LIMIT", 1], ["OFFSET", 1]]
Conversation Load (3.3ms) SELECT "conversations".* FROM "conversations" INNER JOIN "conversation_users" ON "conversation_users"."conversation_id" = "conversations"."id" WHERE "conversation_users"."user_id" = $1 AND "conversation_users"."user_id" = $2 LIMIT $3 [["user_id", "274d2f54-2418-4dec-a696-a5f62196ee85"], ["user_id", "44a0c35a-b5c1-4bb6-a8a3-30ae3d4b3345"], ["LIMIT", 11]]
=> #<ActiveRecord::Relation []>
class Conversation < ApplicationRecord
has_many :user_conversations
has_many :users, through: :user_conversations
def self.between(*users)
users.map do |u|
where("EXISTS(SELECT * FROM user_conversations uc WHERE uc.conversation_id = conversations.id AND uc.user_id = ?)", u)
end.reduce(&:merge)
.joins(:user_conversations)
.group(:id)
.having('count(*) = ?', users.length)
end
end
Not the cleanest solution ever but what is does is check for the existance of a join record for each user and then .having('count(*) = ?', users.length) ensures that there are not more join records then users.
Example usage:
Conversation.between(User.first, User.second)
Conversation.between(1,2,3)
Conversation.between(*User.all) # splat it like a pro
This results in the following SQL query:
SELECT "conversations".* FROM "conversations"
INNER JOIN "user_conversations" ON "user_conversations"."conversation_id" = "conversations"."id"
WHERE
(EXISTS(SELECT * FROM user_conversations uc WHERE uc.conversation_id = conversations.id AND uc.user_id = 1))
AND
(EXISTS(SELECT * FROM user_conversations uc WHERE uc.conversation_id = conversations.id AND uc.user_id = 2))
GROUP BY "conversations"."id"
HAVING (count(*) = 2)
LIMIT $1
You can do a query with AND condition as follows
Conversation.joins(:conversation_users).where(conversation_users: {user_id: User.first.id}).where(conversation_users: {user_id: User.second.id})

How do you return a single value from a scope on a has_many relation?

How do I return a single record from this scope? I tried both ways.
class Subscription < ApplicationRecord
has_many :invoices, dependent: :destroy
class Invoice < ApplicationRecord
belongs_to :subscription
scope :current, -> do
# where(paid: nil).order(:created_at).last
where(paid: nil).order(created_at: :desc).limit(1).first
end
The first way correctly adds order by ... desc limit 1, but then it executes another query without the where condition!
irb(main):004:0> s.invoices.current
Invoice Load (22.0ms) SELECT "invoices".* FROM "invoices" WHERE "invoices"."subscription_id" = $1 AND "invoices"."paid" IS NULL ORDER BY "invoices"."created_at" DESC LIMIT $2 [["subscription_id", 16], ["LIMIT", 1]]
Invoice Load (2.0ms) SELECT "invoices".* FROM "invoices" WHERE "invoices"."subscription_id" = $1 [["subscription_id", 16]]
=> #<ActiveRecord::AssociationRelation [#<Invoice id: 8, subscription_id: 16, user_id: 21, paid: "2018-03-15", created_at: "2018-03-14 22:42:48">]>
The second way also does another query, obliterating the correct results.
irb(main):007:0> s.invoices.current
Invoice Load (2.0ms) SELECT "invoices".* FROM "invoices" WHERE "invoices"."subscription_id" = $1 AND "invoices"."paid" IS NULL
ORDER BY "invoices"."created_at" DESC LIMIT $2 [["subscription_id", 16], ["LIMIT", 1]]
Invoice Load (2.0ms) SELECT "invoices".* FROM "invoices" WHERE "invoices"."subscription_id" = $1 [["subscription_id", 16]]
=> #<ActiveRecord::AssociationRelation [#<Invoice id: 8, subscription_id: 16, user_id: 21, paid: "2018-03-15", created_at: "2018-03-14 22:42:48">]>
Also, how do I get just the record, not an ActiveRecord::AssociationRelation?
Ruby 5.0.6
You might try something like:
class Invoice < ApplicationRecord
belongs_to :subscription
class << self
def for_subscription(subscription)
where(subscription: subscription)
end
def unpaid
where(paid: nil)
end
def newest
order(created_at: :desc).first
end
end
end
Which, if you have an instance of Subscription called #subscription you could use like:
Invoice.unpaid.for_subscription(#subscription).newest
I believe that should fire only one query and should return an invoice instance.
I replaced the scope with
def self.current
where(paid: nil).order(:created_at).last
end
And it worked. However I don't know why, as this method is in Invoice class, while the relation is ActiveRecord::Associations::CollectionProxy class. I wish I knew why it works. And I wish I knew why scope didn't work and performed two queries.

Rails 5: check if role ID belongs to user belonging to current_user (through association)

In my controllers/common/roles_controller.rb I would like to check if particular role (ID) belongs to current_user company users and if not, redirect to errors_path:
def correct_role
role_user = Role.where(:id => params[:id]).select('user_id').first
company_user = current_user.companies.includes(:users)
redirect_to(errors_path) unless company_user.include? role_user.id
end
Definitions:
role_user - finds user ID for particular role ID ("user_id" is column of roles table)
company_user - finds all user ID who belong to companies, which belong to current_user
models/role.rb
belongs_to :user, optional: true, inverse_of: :roles
accepts_nested_attributes_for :user
enum general: { seller: 1, buyer: 2, seller_buyer: 3}, _suffix: true
enum dashboard: { denied: 0, viewer: 1, editer: 2, creater: 3, deleter: 4}, _suffix: true
models/user.rb
#User has roles
has_many :roles
accepts_nested_attributes_for :roles, reject_if: proc { |attributes| attributes[:name].blank? }
# User has many companies
has_many :accounts, dependent: :destroy
has_many :companies, through: :accounts
models/account.rb
class Account < ApplicationRecord
belongs_to :company
belongs_to :user
accepts_nested_attributes_for :company, :user
end
models/company.rb
has_many :accounts, dependent: :destroy
has_many :users, through: :accounts
At the moment with role_user and company_user I can find both ID, however I cannot do the checking part. How do I do that correctly, please? Thank you for any help!
Update
#sajan code give this in console when I open /common/roles/1/edit (current_user ID=1 and should be allowed to edit):
Parameters: {"id"=>"1"}
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
(0.6ms) SELECT COUNT(*) FROM "companies" INNER JOIN "accounts" ON "companies"."id" = "accounts"."company_id" WHERE "accounts"."user_id" = ? [["user_id", 1]]
(0.3ms) SELECT "roles".id FROM "roles" WHERE "roles"."user_id" = ? [["user_id", 1]]
#Abhishek Kumar code in console gives:
Parameters: {"id"=>"1"}
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
Company Load (0.5ms) SELECT "companies".* FROM "companies" INNER JOIN "accounts" ON "companies"."id" = "accounts"."company_id" WHERE "accounts"."user_id" = ? [["user_id", 1]]
(0.4ms) SELECT "users".id FROM "users" INNER JOIN "accounts" ON "users"."id" = "accounts"."user_id" WHERE "accounts"."company_id" = ? [["company_id", 13]]
Role Load (0.2ms) SELECT "roles"."user_id" FROM "roles" WHERE "roles"."id" = ? ORDER BY "roles"."id" ASC LIMIT ? [["id", 1], ["LIMIT", 1]]
Update v2
So I'm trying to use this code:
def correct_role
company_user_ids = current_user.companies.map(&:user_ids)
role_user = Role.where(:id => params[:id]).select('user_id').first
unless role_user.user_id.in?(company_user_ids)
redirect_to(errors_path)
end
end
however it redirects to errors_path in any case, this is what I have in console:
Parameters: {"id"=>"1"}
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
Company Load (0.5ms) SELECT "companies".* FROM "companies" INNER JOIN "accounts" ON "companies"."id" = "accounts"."company_id" WHERE "accounts"."user_id" = ? [["user_id", 1]]
(0.4ms) SELECT "users".id FROM "users" INNER JOIN "accounts" ON "users"."id" = "accounts"."user_id" WHERE "accounts"."company_id" = ? [["company_id", 13]]
Role Load (0.3ms) SELECT "roles"."user_id" FROM "roles" WHERE "roles"."id" = ? ORDER BY "roles"."id" ASC LIMIT ? [["id", 1], ["LIMIT", 1]]
It seems that a better logic would be to define on the model for the current_user an association through companies and users to roles.
Then you can check:
def correct_role
redirect_to(errors_path) unless current_user.company_user_roles.where(id: params[:id]).exists?
end
It would be a single SQL statement that would return zero or one rows, and execute very quickly with the appropriate indexes in place.
Edit:
To company.rb, add:
has_many :user_roles, through: :users, source: :roles
To user.rb (assuming that current_user is an instance of this model) add:
has_many :company_user_roles, through: :companies, source: :user_roles
Maybe you could do like this:
def correct_role
unless current_user.companies.count > 0 && current_user.role_ids.include?(params[:id])
redirect_to(errors_path)
end
end

Rails: How to call one-to-one relationship in rails

I'm new to rails and I want to know how to fetch a one-to-one relationship. I want to fetch users city. In my postgresql database I have:
cities Table:
city:varchar
zipcode: integer
users Table
name:varchar
city_id:int
and in city and user model I have:
class City < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_one :city
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
end
I tried the following in my search controller but didnt work, when logged in:
current_user.city
I get the following error
Processing by SearchController#index as HTML
Parameters: {"utf8"=>"✓", "q"=>"", "criteria"=>"1", "commit"=>"Search"}
User Load (1.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = 6 ORDER BY "users"."id" ASC LIMIT 1
PG::UndefinedColumn: ERROR: column cities.user_id does not exist
LINE 1: SELECT "cities".* FROM "cities" WHERE "cities"."user_id" =...
^
: SELECT "cities".* FROM "cities" WHERE "cities"."user_id" = $1 LIMIT 1
Completed 500 Internal Server Error in 11ms
ActiveRecord::StatementInvalid (PG::UndefinedColumn: ERROR: column cities.user_id does not exist
LINE 1: SELECT "cities".* FROM "cities" WHERE "cities"."user_id" =...
^
: SELECT "cities".* FROM "cities" WHERE "cities"."user_id" = $1 LIMIT 1):
why am I suppose to add a user_id column to cities table, when I have cities foreign key in users table? I dont want to add user_id into cities table.
You can use has_one :through association with join table. Some example for you below.
user model:
class User < ActiveRecord::Base
has_one :city, through: :user_city
has_one :user_city
end
city model:
class City < ActiveRecord::Base
belongs_to :user
end
user city join model:
class UserCity < ActiveRecord::Base
belongs_to :city
belongs_to :user
end
migration for join tables:
class JoinUserCity < ActiveRecord::Migration
def change
create_table :user_cities do |t|
t.integer :user_id
t.integer :city_id
end
end
end
Test in rails console:
=> u = User.create
(0.1ms) begin transaction
SQL (0.5ms) INSERT INTO "users" ("created_at", "updated_at") VALUES (?, ?) [["created_at", "2014-12-07 15:47:14.595728"], ["updated_at", "2014-12-07 15:47:14.595728"]]
(3.3ms) commit transaction
=> #<User id: 4, created_at: "2014-12-07 15:47:14", updated_at: "2014-12-07 15:47:14">
=> u.city
City Load (0.2ms) SELECT "cities".* FROM "cities" INNER JOIN "user_cities" ON "cities"."id" = "user_cities"."city_id" WHERE "user_cities"."user_id" = ? LIMIT 1 [["user_id", 4]]
=> nil
=> c = City.create
(0.1ms) begin transaction
SQL (0.5ms) INSERT INTO "cities" ("created_at", "updated_at") VALUES (?, ?) [["created_at", "2014-12-07 15:47:24.535039"], ["updated_at", "2014-12-07 15:47:24.535039"]]
(3.3ms) commit transaction
=> #<City id: 1, created_at: "2014-12-07 15:47:24", updated_at: "2014-12-07 15:47:24">
irb(main):004:0> u.city = c
UserCity Load (0.3ms) SELECT "user_cities".* FROM "user_cities" WHERE "user_cities"."user_id" = ? LIMIT 1 [["user_id", 4]]
(0.1ms) begin transaction
SQL (0.4ms) INSERT INTO "user_cities" ("city_id", "user_id") VALUES (?, ?) [["city_id", 1], ["user_id", 4]]
(1.0ms) commit transaction
=> #<City id: 1, created_at: "2014-12-07 15:47:24", updated_at: "2014-12-07 15:47:24">
irb(main):005:0> u.save
(0.1ms) begin transaction
(0.1ms) commit transaction
=> true
=> u = User.last
User Load (0.3ms) SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT 1
=> #<User id: 4, created_at: "2014-12-07 15:47:14", updated_at: "2014-12-07 15:47:14">
=> u.city
City Load (0.2ms) SELECT "cities".* FROM "cities" INNER JOIN "user_cities" ON "cities"."id" = "user_cities"."city_id" WHERE "user_cities"."user_id" = ? LIMIT 1 [["user_id", 4]]
=> #<City id: 1, created_at: "2014-12-07 15:47:24", updated_at: "2014-12-07 15:47:24">
take a look at the document of has_one and belogns_to,
belongs_to(name, options = {})
Specifies a one-to-one association with another class. This method should only be used if this class
contains the foreign key. If the other class contains the foreign key, then you should use has_one
instead.
as the user table has the foreign key, you should change your model definition like this
class City < ActiveRecord::Base
has_one :user
end
class User < ActiveRecord::Base
belongs_to :city
end

Resources