How to get ActiveRecord non-persistant variable to save? - ruby-on-rails

I'm trying to setup an attribute that isn't saved to the database but I can't work out how to change it and read the new value.
class User < ApplicationRecord
attribute :online, :boolean, default: false
end
in Rails Console:
User.first.online = true
=> true
User.first.online
=> false
I'm running Ruby-on-rails 5.2.4.1 and ruby 2.4.1
https://api.rubyonrails.org/v5.2.4.1/classes/ActiveRecord/Attributes/ClassMethods.html#method-i-attribute

The line:
User.first
Creates an instance for the first user each time you call it.
User.first.equal?(User.first) #=> false
# ^^^^^^
# Equality — At the Object level, returns true only if obj
# and other are the same object.
You're setting the online attribute of a different instance than the one you're reading from (although they represent the same record). Store the user in a variable. That way you're working with the same instance for both the set and get call.
user = User.first
user.online = true
user.online #=> true

Related

mongoid equivalent of loaded?

In a Rails app, using ActiveRecord with mysql, you can check to see if an association has been loaded:
class A
belongs_to :b
a = A.find(...
a.b.loaded? # returns whether the associated object has been loaded
Is there an equivalent in mongoid? ._loaded? used to work but no longer does.
UPDATE - adding example
class A
include Mongoid::Document
end
class B
include Mongoid::Document
belongs_to :a
end
a = A.new
b = B.new
b.a = a
b.a._loaded?
returns:
ArgumentError (wrong number of arguments (given 0, expected 1))
It's a enumerable method of this Class: Mongoid::Relations::Targets::Enumerable
_loaded?
it will return true and false if Has the enumerable been _loaded? This will be true if the criteria has been executed or we manually load the entire thing.
Maybe the purpose was not the same.
Now (Mongoid 7.0) _loaded?() is a private method, and in my case it always returns true.
The best I could find is ivar(): it returns the object if already loaded, or false if not loaded.
I'm not sure if this is a reliable solution. It depends on further availability of ivar() and the way objects are stored as instance variables.
> b = B.find('xxx')
=> (db request for "b")
> b.ivar('a')
=> false
> b.a
=> (db request for "a")
> b.ivar('a')
=> (returns "a" object, as when b.a is called)
You can test if includes(:your_association) has been added to the criteria like this:
inclusions = Criteria.inclusions.map(&:class_name)
inclusions.include?('YourAssociation)
For example:
Children.all.include(:parent).inclusions
=> [<Mongoid::Association::Referenced::BelongsTo:0x00007fce08c76040
#class_name="Parent", ...]
Children.all.inclusions
=> []

Dynamically extend Virtus instance attributes

Let's say we have a Virtus model User
class User
include Virtus.model
attribute :name, String, default: 'John', lazy: true
end
Then we create an instance of this model and extend from Virtus.model to add another attribute on the fly:
user = User.new
user.extend(Virtus.model)
user.attribute(:active, Virtus::Attribute::Boolean, default: true, lazy: true)
Current output:
user.active? # => true
user.name # => 'John'
But when I try to get either attributes or convert the object to JSON via as_json(or to_json) or Hash via to_h I get only post-extended attribute active:
user.to_h # => { active: true }
What is causing the problem and how can I get to convert the object without loosing the data?
P.S.
I have found a github issue, but it seems that it was not fixed after all (the approach recommended there doesn't work stably as well).
Building on Adrian's finding, here is a way to modify Virtus to allow what you want. All specs pass with this modification.
Essentially, Virtus already has the concept of a parent AttributeSet, but it's only when including Virtus.model in a class.
We can extend it to consider instances as well, and even allow multiple extend(Virtus.model) in the same object (although that sounds sub-optimal):
require 'virtus'
module Virtus
class AttributeSet
def self.create(descendant)
if descendant.respond_to?(:superclass) && descendant.superclass.respond_to?(:attribute_set)
parent = descendant.superclass.public_send(:attribute_set)
elsif !descendant.is_a?(Module)
if descendant.respond_to?(:attribute_set, true) && descendant.send(:attribute_set)
parent = descendant.send(:attribute_set)
elsif descendant.class.respond_to?(:attribute_set)
parent = descendant.class.attribute_set
end
end
descendant.instance_variable_set('#attribute_set', AttributeSet.new(parent))
end
end
end
class User
include Virtus.model
attribute :name, String, default: 'John', lazy: true
end
user = User.new
user.extend(Virtus.model)
user.attribute(:active, Virtus::Attribute::Boolean, default: true, lazy: true)
p user.to_h # => {:name=>"John", :active=>true}
user.extend(Virtus.model) # useless, but to show it works too
user.attribute(:foo, Virtus::Attribute::Boolean, default: false, lazy: true)
p user.to_h # => {:name=>"John", :active=>true, :foo=>false}
Maybe this is worth making a PR to Virtus, what do you think?
I haven't investigated it further, but it seems that every time you include or extend Virtus.model, it initializes a new AttributeSet and set it to #attribute_set instance variable of your User class (source). What the to_h or attributes do is they call the get method of the new attribute_set instance (source). Therefore, you can only get attributes after the last inclusion or the extension of Virtus.model.
class User
include Virtus.model
attribute :name, String, default: 'John', lazy: true
end
user = User.new
user.instance_variables
#=> []
user.send(:attribute_set).object_id
#=> 70268060523540
user.extend(Virtus.model)
user.attribute(:active, Virtus::Attribute::Boolean, default: true, lazy: true)
user.instance_variables
#=> [:#attribute_set, :#active, :#name]
user.send(:attribute_set).object_id
#=> 70268061308160
As you can see, the object_id of attribute_set instance before and after the extension is different which means that the former and the latter attribute_set are two different objects.
A hack I can suggest for now is this:
(user.instance_variables - [:#attribute_set]).each_with_object({}) do |sym, hash|
hash[sym.to_s[1..-1].to_sym] = user.instance_variable_get(sym)
end

Unexpected output in rails when using scopes

Environment - Rails 4.2, Ruby 2.3
Trying to play with scopes and created one that will return true or false for the user.superuser of the current_user, from inside the non-User Model/Controller. I am getting the expected output when current_user.superuser = true but false just goes out left field.
Breakdown:
appliance.rb (model)
scope :superuser, ->(user) { user.superuser ? true : false }
appliance_controller.rb
def list
#appliances = Appliance.all.order(:idtag)
#users = User.all
end
list.html.haml
%h5
= "Is the user a superuser? #{Appliance.superuser(current_user).to_s}"
Rails console for when querying a user that has the superuser attribute set to true
irb(main):006:0* current_user = User.first User Load ... #<User id: 3, superuser: true>
irb(main):007:0> Appliance.superuser(current_user)
=> true
Rails console for when querying a user that has the superuser attribute set to false
irb(main):008:0> current_user = User.last User Load # User id: 6, superuser: false>
irb(main):010:0* Appliance.superuser(current_user)
Appliance Load (4.1ms) SELECT "appliances".* FROM "appliances"
=>#ActiveRecord::Relation [#Appliance id:1, ... updated_at: "...">, #Appliance id:2, ... updated_at: "...">]>
Basically it's dumping Appliance.all and returns an ActiveRecord_relation instead of false. Can anyone explain why this is happening?
As per the rails api :
Scope Adds a class method for retrieving and querying objects. The method is intended to return an ActiveRecord::Relation object, which is composable with other scopes. If it returns nil or false, an all scope is returned instead.
So this is not unexpected, rather a exact expected behavior. your scope returns false, it applies all scope and dumps all of the Appliance records, which are activerecord relation objects, as stated in api.

Reset value of an attribute to default

I would like to know how I can set a models attribute and possible associations to its default value.
user = User.find_by(name: "Martin")
user.phone = 012345
user.save!
# some time later
user.phone = # set to default
user.save!
Few options to set a default value of a column:
Set the default value in migration (preferable)
Set the default value in before_* callback
To revert to default column's value you can use ActiveRecord::ConnectionAdapters::SchemaCache#columns_hash:
user.phone = user.class.columns_hash['phone'].default
You already set default in the migration.
:default => 'your_default'
It's better to use:
User.column_defaults["phone"]
instead of:
User.columns_hash['phone'].default
since columns_hash gets the raw default value defined at database level and skips defaults set in ActiveModel. See the following example:
class Order < ApplicationRecord
enum status: %i[open closed]
attribute :deliver_at, default: -> { Date.tomorrow }
end
Order.columns_hash['status'].default # => "0" ('0' if default value was defined in the database or 'nil' otherwise)
Order.columns_hash['deliver_at'].default # => NoMethodError (undefined method `default' for nil:NilClass) if it's a virtual attribute or 'nil' if the column exists in the database
Order.column_defaults['status'] # => "open"
Order.column_defaults['deliver_at'] # => Wed, 06 May 2020

Rails 4 to 5 AR boolean deprecation

I have a model wish contains the bellow method called by before_validation :
def set_to_false
self.confirme ||= false
self.deny ||= false
self.favoris ||= false
self.code_valid ||= false
end
When I run my tests, I got the deprecation message
DEPRECATION WARNING: You attempted to assign a value which is not
explicitly true or false to a boolean column. Currently this value
casts to false. This will change to match Ruby's semantics, and will
cast to true in Rails 5. If you would like to maintain the current
behavior, you should explicitly handle the values you would like cast
to false. (called from cast_value at
./Ruby2.1.0/lib/ruby/gems/2.1.0/gems/activerecord-4.2.1/lib/active_record/type/boolean.rb:17)
I understand I have to cast but I couldn't find a simple and smart way to do it. Any help to remove this deprecation would be great.
Here's a simple booleanification trick that I use often, double negation:
before_validation :booleanify
def booleanify
self.confirm = !!confirm
self.deny = !!deny
...
end
In case you are not familiar with this trick, it'll convert all values to their boolean equivalents, according to ruby rules (nil and false become false, everything else becomes true)
'foo' # => "foo"
!'foo' # => false
!!'foo' # => true
!nil # => true
!!nil # => false

Resources