How to compare Rails enum types - ruby-on-rails

I have the following enum model in my Rails (4) application:
class Dual < ActiveRecord::Base
enum dual: [:dual, :not_dual]
validates :dual, uniqueness: true
validates :dual, presence: true
end
And I have another model which has many Duals:
class SillColour < ActiveRecord::Base
has_many :sill_colour_duals, dependent: :destroy
has_many :duals, through: :sill_colour_duals
end
I want to be able to test if an instance of SillColour has a Dual enum. This is all I could get to work:
dual = Dual.find(1)
not_dual = Dual.find(2)
sill_colour.duals.include?(dual)
sill_colour.duals.include?(not_dual)
Obviously this is extremely unreliable as the ID of the Duals could be anything in production (for testing IDs are fixed). I tried this:
dual = Dual.where(dual: 0)
not_dual = Dual.where(dual: 1)
and even given the database duals table looks like this:
id | dual
----+------
1 | 0
2 | 1
My tests fail and it seems to be because dual and non_dual are no longer comparing correctly. I've examined them using pry and they appear to be the same as before, but clearly they're not.
Surely there must be a better way? I envisaged being able to do this:
sill_colour.duals.include?(Dual.dual)
sill_colour.duals.include?(Dual.not_dual)
but this doesn't work either.
Any suggestions?

I'll try to answer with a code from my app
class User < ActiveRecord::Base
has_many :contacts
end
class Contact < ActiveRecord::Base
enum status: [:dual, :not_dual]
# see scopes
scope :dual, -> {where(status: Contact.statuses['dual']) }
scope :not_dual, -> {where(status: Contact.statuses['not_dual']) }
end
Now console, check if a User instance has a contact with dual status:
2.1.5 :001 > u = User.last
User Load (0.8ms) SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT 1
=> #<User id: 1, email: "...", ...>
2.1.5 :003 > u.contacts.dual
Contact Load (0.3ms) SELECT "contacts".* FROM "contacts" WHERE "contacts"."user_id" = $1 AND "contacts"."status" = 0 [["user_id", 1]]
=> #<ActiveRecord::AssociationRelation []>
2.1.5 :004 > u.contacts.not_dual
Contact Load (0.3ms) SELECT "contacts".* FROM "contacts" WHERE "contacts"."user_id" = $1 AND "contacts"."status" = 1 [["user_id", 1]]
=> #<ActiveRecord::AssociationRelation []>
As you can see both return empty array, so calling any? on empty array will return false both. Because I don't have any contact with status dual or not_dual, let's create one.
Find a contact:
2.1.5 :005 > c = Contact.last
Contact Load (0.5ms) SELECT "contacts".* FROM "contacts" ORDER BY "contacts"."id" DESC LIMIT 1
=> #<Contact id: 3, user_id: 1, ..., ...., status: nil>
Set it as dual:
2.1.5 :006 > c.dual!
(0.1ms) BEGIN
SQL (15.1ms) UPDATE "contacts" SET "status" = $1, "updated_at" = $2 WHERE "contacts"."id" = 3 [["status", 0], ["updated_at", "2014-12-29 13:16:45.576778"]]
(25.5ms) COMMIT
=> true
Now check if user has dual or not_dual contacts:
2.1.5 :009 > u.contacts.dual.any?
(0.2ms) SELECT COUNT(*) FROM "contacts" WHERE "contacts"."user_id" = $1 AND "contacts"."status" = 0 [["user_id", 1]]
=> true
2.1.5 :010 > u.contacts.not_dual.any?
(0.3ms) SELECT COUNT(*) FROM "contacts" WHERE "contacts"."user_id" = $1 AND "contacts"."status" = 1 [["user_id", 1]]
=> false
2.1.5 :011 >
Checking if user instance has a dual enum returns true. In your case instead of user it will be sill_colour.
If you don't like scopes you can use where:
u.contacts.where(status: Contact.statuses['dual']).any?
=> true
http://edgeapi.rubyonrails.org/classes/ActiveRecord/Enum.html
class Dual < ActiveRecord::Base
enum status: [:dual, :not_dual]
end
Having the class as the one above you can check the status like this:
dual = Dual.find(params[:id])
dual.dual? # will return true or false depends on the status you set.
dual.not_dual? # same, true or false
dual.status = "dual" # if status was set to 0 or not_dual if status was set to 1

Related

N+1 in has_many :through

I ran into problem N + 1
in association :
class Category < ApplicationRecord
has_many :categories_designs, dependent: :destroy
has_many :designs, through: :categories_designs
has_many :templates, ->{ where(is_template: true) }, through: :categories_designs, class_name: 'Design', source: :design
def marked_designs_as_new?
designs.select(:mark_design_as_new_until).where("mark_design_as_new_until >= ?", Time.now.in_time_zone.beginning_of_day).exists?
end
end
And I want to use the marked_designs_as_new? method in the view.
- #categories.each do |category|
= category.title.titleize
- if category.marked_designs_as_new?
.design-type-marked
NEW
In my controller I call:
#categories = Category.includes(categories_designs: :design).visible
And I'm faced with the problem of N + 1.
Category Load (0.4ms) SELECT "categories".* FROM "categories" WHERE "categories"."hidden" = $1 ORDER BY "categories"."position" ASC LIMIT $2 OFFSET $3 [["hidden", false], ["LIMIT", 100], ["OFFSET", 0]]
CategoriesDesign Load (0.4ms) SELECT "categories_designs".* FROM "categories_designs" WHERE "categories_designs"."category_id" IN (1, 3, 4, 5, 6, 7, 8)
Design Load (0.5ms) SELECT "designs".* FROM "designs" WHERE "designs"."id" IN (1, 4, 3, 6)
(0.7ms) SELECT COUNT(*) FROM "designs" INNER JOIN "categories_designs" ON "designs"."id" = "categories_designs"."design_id" WHERE "categories_designs"."category_id" = $1 AND "designs"."is_template" = $2 [["category_id", 1], ["is_template", true]]
Design Exists (0.7ms) SELECT 1 AS one FROM "designs" INNER JOIN "categories_designs" ON "designs"."id" = "categories_designs"."design_id" WHERE "categories_designs"."category_id" = $1 AND (mark_design_as_new_until >= '2018-03-13 00:00:00') LIMIT $2 [["category_id", 1], ["LIMIT", 1]]
(0.5ms) SELECT COUNT(*) FROM "designs" INNER JOIN "categories_designs" ON "designs"."id" = "categories_designs"."design_id" WHERE "categories_designs"."category_id" = $1 AND "designs"."is_template" = $2 [["category_id", 3], ["is_template", true]]
............. etc.
why?
Ok, your .select(:mark_design_as_new_until) performs another query to the database. What you should do is use an array select method in the following way:
.select(&:mark_design_as_new_until)
This gives you an array of designs loaded in the memory on which you can perform .any? method to check your condition:
.select(&:mark_design_as_new_until).any? { |design| design.mark_design_as_new_until >= Time.now.in_time_zone.beginning_of_day }
And of course, include designs in your Category.
Category.includes(:designs, ...)
Did you try Category.includes([:categories_designs, :design]) Also, you can change the marked_designs_as_new? method as follows,
def marked_designs_as_new?
designs.select{ |x| x.marked_designs_as_new? }.any?
end
design.rb
class Design
def marked_designs_as_new?
mark_design_as_new_until >= Time.now.in_time_zone.beginning_of_day
end
end

Rails scope doesn't return correct data

When I use scope which I prepared in model, rails returns incorrect data.
My Model:
class CurrencyRate < ActiveRecord::Base
scope :eur_today, -> {where(currency: "eur").where(date: Time.now.in_time_zone.to_date).first}
end
Inforamation from rails console:
2.3.0 :011 > CurrencyRate.eur_today
CurrencyRate Load (0.2ms) SELECT "currency_rates".* FROM
"currency_rates" WHERE "currency_rates"."currency" = ? AND
"currency_rates"."date" = ? ORDER BY "currency_rates"."id" ASC LIMIT 1
[["currency", "eur"], ["date", "2017-08-09"]]
CurrencyRate Load (0.2ms) SELECT "currency_rates".* FROM
"currency_rates"
=> #<ActiveRecord::Relation [#<CurrencyRate id: 2, currency: "eur",
sale: 4.248546249999998, purchase: 4.265333125, date: "2017-08-08",
created_at: "2017-08-08 20:52:08", updated_at: "2017-08-08 20:54:10",
sale_percentage_diff: nil, purchase_percentage_diff: nil>]>
When I use the same query like in scope, returned data is correct:
2.3.0 :012 > CurrencyRate.where(currency: "eur").where(date: Time.now.in_time_zone.to_date).first
CurrencyRate Load (0.2ms) SELECT "currency_rates".* FROM
"currency_rates" WHERE "currency_rates"."currency" = ? AND
"currency_rates"."date" = ? ORDER BY "currency_rates"."id" ASC LIMIT 1
[["currency", "eur"], ["date", "2017-08-09"]]
=> nil
Why scope doesn't work correct?
Because scopes must return an ActiveRecord::Relation. Get rid of the first call in your scope and use that outside the scope. Why? Because scopes have to be chainable.

Lock value of database column based on boolean value in Rails

I want to let a user edit the field report.plan only if report.published = false. If report.published = true and they try to save a change, I want to throw an error.
I've written the following code to do this:
class Report < ActiveRecord::Base
validate :cannot_update_plan_after_published, on: :publish_plan!
def publish_plan!(plan)
self.plan = plan
self.published = true
self.save
end
private
def cannot_update_plan_after_published
if self.published?
errors.add(:plan, "You cannot change the plan once it has been published.")
end
end
end
However, this is not working. When I call publish_plan! on an already published report, it makes the save. For example:
> f = Report.last
=> #<Report id: 12, plan: "a", published: false>
> f.publish_plan!("b")
(0.1ms) begin transaction
(0.4ms) UPDATE "reports" SET "plan" = 'b', "updated_at" = '2014-09-18 18:43:47.459983' WHERE "reports"."id" = 12
(9.2ms) commit transaction
=> true
> f = Report.last
Report Load (0.2ms) SELECT "reports".* FROM "reports" ORDER BY "reports"."id" DESC LIMIT 1
=> #<Report id: 12, plan: "b", published: true>
> f.publish_plan!("c")
(0.1ms) begin transaction
(0.4ms) UPDATE "reports" SET "plan" = 'c', "updated_at" = '2014-09-18 18:43:53.996191' WHERE "reports"."id" = 12
(8.7ms) commit transaction
=> true
> Report.last
Report Load (0.2ms) SELECT "reports".* FROM "reports" ORDER BY "reports"."id" DESC LIMIT 1
=> #<Report id: 12, plan: "c", published: true>
How do I get this field to become uneditable once report.published = true?
Try removing the on: :public_plan!. That way, the validation should be run every time the model is saved.
validate :cannot_update_plan_after_published
See here for more details: Adding a validation error with a before_save callback or custom validator?
Also, for the validation method itself, change it to the following:
def cannot_update_plan_after_published
if self.published? && self.published_changed? == false
errors.add(:plan, "You cannot change the plan once it has been published.")
end
end
This allows you to set it the first time publishing the plan.

How to check for database changes of in-memory records?

I want to check if an ActiveRecord instance was changed database-wise. Something like:
p1 = Product.first
p1.name #=> "some name"
p2 = Product.first
p2.name = "other name"
p2.save #=> true
p1.database_changed? #=> true
I'm currently comparing the record's attributes to the persisted attributes:
class Product < ActiveRecord::Base
def database_changed?
Product.find(id).attributes != attributes
end
end
This seems to work, but I'm wondering if there is a built-in way to find database changes?
After Зелёный's comment I've reviewed ActiveModel::Dirty and realized that it almost does what I want. There's already an in-memory state (the record's attributes) and a database state (handled by ActiveModel::Dirty). I just need a method to update the database state, leaving the in-memory state unchanged:
def refresh
#changed_attributes = {}
fresh_object = self.class.unscoped { self.class.find(id) }
fresh_object.attributes.each do |attr, orig_value|
#changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, #attributes[attr])
end
self
end
#changed_attributes is ActiveModel::Dirty's hash to store changed values. We obviously have to reset it.
fresh_object is the same record, freshly fetched from the database (this line comes from reload, thanks emaillenin).
Within the loop, each (fresh) attribute is compared to the corresponding (in-memory) attribute. If they differ, it is added to the #changed_attributes hash. This line comes from ActiveRecord's dup method. (it's actually from a private method called by dup, and _field_changed is private, too. It might be better to use ActiveRecord's public API, but I was lazy)
Finally, refresh returns self for convenience, just like reload does.
Here's an example usage:
p1 = Product.first
p1.name #=> "some name"
p1.changed? #=> false
p2 = Product.first
p2.name = "other name"
p2.save #=> true
p1.refresh
p1.name #=> "some name"
p1.changed? #=> true
p1.name_changed? #=> true
p1.name_was #=> "other name"
p1.name = "other name"
p1.name_changed? #=> false
p1.changed? #=> true
p1.changes #=> {"updated_at"=> [Tue, 29 Jul 2014 21:58:57 CEST +02:00, Tue, 29 Jul 2014 15:49:54 CEST +02:00]}
def database_changed?
self.class.where(self.class.arel_table[:updated_at].gt(updated_at)).exists? self
end
The Rails way to to do this is to use reload method on the ActiveRecord object.
def database_changed?
attributes != reload.attributes
end
Terminal 1
2.1.2 :001 > c = Car.find(1)
Car Load (0.4ms) SELECT "cars".* FROM "cars" WHERE "cars"."id" = ? LIMIT 1 [["id", 1]]
=> #<Car id: 1, name: "Audi", model: "R8", created_at: "2014-07-29 11:14:43", updated_at: "2014-07-29 11:14:43">
2.1.2 :002 > c.database_changed?
Car Load (0.1ms) SELECT "cars".* FROM "cars" WHERE "cars"."id" = ? LIMIT 1 [["id", 1]]
=> false
2.1.2 :003 > c.database_changed?
Car Load (0.2ms) SELECT "cars".* FROM "cars" WHERE "cars"."id" = ? LIMIT 1 [["id", 1]]
=> false
2.1.2 :004 > c.database_changed?
Car Load (0.2ms) SELECT "cars".* FROM "cars" WHERE "cars"."id" = ? LIMIT 1 [["id", 1]]
=> true
Terminal 2
2.1.2 :001 > c = Car.find(1)
Car Load (0.2ms) SELECT "cars".* FROM "cars" WHERE "cars"."id" = ? LIMIT 1 [["id", 1]]
=> #<Car id: 1, name: "Audi", model: "R8", created_at: "2014-07-29 11:14:43", updated_at: "2014-07-29 11:14:43">
2.1.2 :002 > c.model = 'A5'
=> "A5"
2.1.2 :003 > c.save!
(0.2ms) begin transaction
SQL (0.3ms) UPDATE "cars" SET "model" = ?, "updated_at" = ? WHERE "cars"."id" = 1 [["model", "A5"], ["updated_at", "2014-07-29 11:15:32.845875"]]
(1.2ms) commit transaction
=> true
2.1.2 :004 >

Rails association skip update associated object

class City<ActiveRecord::Base
has_one :template, class_name:'TmplLocation'
after_initialize :_init
private
def _init
self.template = TmplLocation.find(18) if !self.template
end
end
And that's what happens in console:
>Loc.first.template
City Load (29.8ms) SELECT `locations`.* FROM `locations` WHERE `locations`.`type` IN ('City') LIMIT 1
TmplLocation Load (0.2ms) SELECT `locations`.* FROM `locations` WHERE `locations`.`type` IN ('TmplLocation') AND `locations`.`location_id` = 23 LIMIT 1
TmplLocation Load (34.8ms) SELECT `locations`.* FROM `locations` WHERE `locations`.`type` IN ('TmplLocation') AND `locations`.`id` = ? LIMIT 1 [["id", 18]]
SQL (0.2ms) BEGIN
(0.7ms) UPDATE `locations` SET `location_id` = 23, `updated_at` = '2013-06-11 10:47:11' WHERE `locations`.`type` IN ('TmplLocation') AND `locations`.`id` = 18
(41.4ms) COMMIT
You see? It updates the TmplLocation so now it is constantly associated with this exact city.
I want only use the TmplLocation instance in this City
How to skip update stage??
You can try something like this
class City<ActiveRecord::Base
has_one :template, class_name:'TmplLocation', :conditions => { :id => 18 }
end
For more options see this
guides.rubyonrails.org

Resources