Rails combine searching by enum and find_or_initialize_by methods - ruby-on-rails

this question is based on this one: Rails find_or_create_by with/without where
I've experienced some strange issue about enum & find_or_initialize_by methods in rails.
Let suppose I have the model Task.
class Task < AR::Base
enum status: { todo: 0, awaiting: 1, done: 2, another_one: 3, et.c. }
I want to find through todo and awaiting tasks, and if there is no exist, create a new one, but without a todo and awaiting value (status should be just nil). But, ActiveRecord::Enum strikes here!
This is the normal code in most cases:
Task.where(status: [Task.statuses[:todo], Task.statuses[:awaiting]]).find_or_initialize_by(title: 'epic')
But because of this string I get annoying '[0, 1]' is not a valid status exception. How can I avoid this exception without superfluous code?

For now I see just one relatively cool solution. Will be great if somebody another gives solution better.
Install gem 'active_record_union'
Use it!
Task.todo.union(Task.awaiting).union(Task.starting)

When querying an enum attribute you should use the symbol representing the enum and not the actual integer column value:
#task = Task.where(status: [:todo, :awaiting])
.find_or_initialize_by(title: 'epic')
Hence the '[0, 1]' is not a valid status since Rails is trying to be helpful and look up the enum values for you. In general you almost never need to use the actual integer value unless your are building a custom SQL string:
Task.where.not(status: :awaiting)
Rails also generates scopes based on the enum:
#tasks = Task.todo.awaiting.find_or_initialize_by(title: 'epic')
# or
#tasks = Task.todo.merge(Task.awaiting)
.find_or_initialize_by(title: 'epic')

Related

Query Rails Wildcard Enum Type

Trying to figure out how to utilize a "wildcard" feature with rails enums, where I don't want to have a dedicated int/symbol mapping for a number of variations of types.
For example, I have a ticketing system, where the majority of tickets are unanswered or answered, but the user can also set a custom status in special cases.
class Ticket
enum status: { unanswered: 0, answered: 1, *wildcard: 2 }
end
I'd like to be able to query for Ticket.where(status: :wildcard) and return all tickets with a status value of 2. Digging into the rails source code, I think I want to override.
I figure I can skip the assert_valid_value validations by having a before_save callback that converts any non-existent strings to the right status code, but how would I query?

IN query does not filter - Ruby on Rails 4

I am trying to filter by multiple fields. The fields are taken from an ENUM list and put in checkbox fields, then saved into an array. The GET request for what I'm trying to filter looks like this: {"statuses" => ["active", "past_due"]}. I have a scope with the query, which should filter by the values selected. I am assuming the problem is in the model.
The model:
scope :subscription_status, -> (statuses) {where :subscription_status => statuses}
The controller:
#users = User.where(nil)
#users = #users.subscription_status(params[:statuses]) if params[:statuses].present?
The view (html.haml):
= form_tag root_path, :method => 'get' do
- #subscription_statuses.each do |status|
= check_box_tag 'statuses[]', status
= status
= submit_tag "Filter", :name => nil
#subscription_statuses contains the ENUM list.
My other filters work on the same principle, but this one doesn't seem to work. The page reloads, the GET params are sent, but there is no filtering.
Thank you in advance. :)
EDIT:
#subscription_statuses = User.subscription_statuses.keys
This is an array of strings, and the values in the database are saved as strings.
A couple of things come to mind. I had this type of problem before, so I can only assume here that the statues you generating here
= check_box_tag 'statuses[]', status
is string.
So when you pass them into the params params[:statuses], it's an array of string, but when we query on enum type, if it's just a single one, you can pass in either sym or string, it will work fine. However that's not the case when you have multiple ones.
where :subscription_status => statuses will generate the sql something like WHERE users.subscription_status IN (0, 0, 0), but all the subscription_statuses are stored as enum type which are integers.
So in your case, you have to find their corresponding integer value first like this
where(subscription_status: User.subscription_status.slice(*statuses).values)
Also you may find this answer helpful as well
UPDATE
Since you already have subscription_status stored as string in the database, declaring the column as enum type in the User model would potentially create a conflict which resulted in not generating desired query. Simple removal of the enum type declaration would possibly resolve the issue.
UPDATE 2
So I messed around with the enum type in rails console. Regarding the problem of query showing up like IN (0, 0, 0) when you pass in an array of strings. It actually shows IN (NULL, NULL, NULL) when you pass in an array of symbols.
My guess is that, since you have declared it as enum type, it wants each status as integer, so it probably did statuses.map(&:to_i) when you try where(subscription_status: statuses) before it generate the query. As a new finding (yay me!) "hello".to_i => 0, while :hello.to_i => undefined method therefore (0, 0, 0) and (NULL, NULL, NULL). (I hope you understand my messy explanation here)
Possibility of mapping enum values to string type instead of integer
I think you would better try this code in rails console. As it turns out there shouldn't be any problems in your implementation but, taking a look at the generated SQL would be the ideal thing to do. You can use a console debugger like Byebug to help you debug the request and stepping through it.

How to get the id value from a Rails model enum?

Trying to understand how to use enums to help manage the different user status levels.
class User < ActiveRecord::Base
enum user_status: [:active, :pending, :trial]
end
So now active is 0, pending is 1, and trial is 2.
So if I do this:
user.active?
This works fine, but currently when I am setting the value I am still doing this:
user.user_status = 0
I was hoping I could do something like:
user.user_status = User.UserStatus.trial
The point is I don't want to have to remember what index each enum value is.
Also, if I change the order or add more user_status values, the index will change and I want to prevent bugs from me hardcoding the values in my codebase.
Is there a better way to handle the enum index values?
You can easily find the answer simply reading the documentation:
user.trial!
will set the status and update the record. For more variants you can refer to the docs.
it works in next way:
user.trial!
more detail

How to save nil into serialized attribute in Rails 4.2

I am upgrading an app to Rails 4.2 and am running into an issue where nil values in a field that is serialized as an Array are getting interpreted as an empty array. Is there a way to get Rails 4.2 to differentiate between nil and an empty array for a serialized-as-Array attribute?
Top level problem demonstration:
#[old_app]
> Rails.version
=> "3.0.3"
> a = AsrProperty.new; a.save; a.keeps
=> nil
#[new_app]
> Rails.version
=> "4.2.3"
> a = AsrProperty.new; a.save; a.keeps
=> []
But it is important for my code to distinguish between nil and [], so this is a problem.
The model:
class AsrProperty < ActiveRecord::Base
serialize :keeps, Array
#[...]
end
I think the issue lies with Rails deciding to take a shortcut for attributes that are serialized as a specific type (e.g. Array) by storing the empty instance of that type as nil in the database. This can be seen by looking at the SQL statement executed in each app:
[old_app]: INSERT INTO asr_properties (lock_version, keeps)
VALUES (0, NULL)
Note that the above log line has been edited for clarity; there are other serialized attributes that were being written due to old Rails' behavior.
[new_app]: INSERT INTO asr_properties (lock_version)
VALUES (0)
There is a workaround: by removing the "Array" declaration on the serialization, Rails is forced to save [] and {} differently:
class AsrProperty < ActiveRecord::Base
serialize :keeps #NOT ARRAY
#[...]
end
Changing the statement generated on saving [] to be:
INSERT INTO asr_properties (keeps, lock_version) VALUES ('---[]\n', 0)
Allowing:
> a = AsrProperty.new; a.save; a.keeps
=> nil
I'll use this workaround for now, but:
(1) I feel like declaring a type might allow more efficiency, and also prevents bugs by explicitly prohibiting the wrong data type being stored
(2) I'd really like to figure out the "right" way to do it, if Rails does allow it.
So: can Rails 4.2 be told to store [] as its own thing in a serialized-as-Array attribute?
What's going on?
What you're experiencing is due to how Rails 4 treats the 2nd argument to the serialize call. It changes its behavior based on the three different values the argument can have (more on this in the solution). The last branch here is the one we're interested in as when you pass the Array class, it gets passed to the ActiveRecord::Coders::YAMLColumn instance that is created. The load method receives the YAML from the database and attempts to turn it back into a Ruby object here. If the coder was not given the default class of Object and the yaml argument is nil in the case of a null column, it will return a new instance of the class, hence the empty array.
Solution
There doesn't appear to be a simple Rails-y way to say, "hey, if this is null in the database, give me nil." However looking at the second branch here we see that we can pass any object that implements the load and dump methods or what I call the basic coder protocol.
Example code
One of the members of my team built this simple class to handle just this case.
class NullableSerializer < ActiveRecord::Coders::YAMLColumn
def load(yaml)
return nil if yaml.nil?
super
end
end
This class inherits from the same YAMLColumn class provided by ActiveRecord so it already handles the load and dump methods. We do not need any modifications to dump but we want to slightly handle loading differently. We simply tell it to return nil when the database column is empty and otherwise call super to work as if we made no other modification.
Usage
To use it, it simply needs to be instantiated with your intended serialization class and passed to the Rails serialize method as in the following, using your naming from above:
class AsrProperty < ActiveRecord::Base
serialize :keeps, NullableSerializer.new(Array)
# …
end
The "right" way
Getting things done and getting your code shipped is paramount and I hope this helps you. After all, if the code isn't being used and doing good, who cares how ideal it is?
I would argue that Rails' approach is the right way in this case especially when you take Ruby's philosophy of The Principle of Least Surprise into account. When an attribute can possibly be an array, it should always return that type, even if empty, to avoid having to constantly special case nil. I would argue the same for any database column that you can put a reasonable default on (i.e. t.integer :anything_besides_a_foreign_key, default: 0). I've always been grateful to past-Aaron for remembering this most of the time whenever I get an unexpected NoMethodError: undefined method 'whatever' for nil:NilClass. Almost always my special case for this nil is to supply a sensible default.
This varies greatly on you, your team, your app, and your application and it's needs so it's never hard and fast. It's just something I've found helps me out immensely when I'm working on something and wondering if amount could default to 0 or if there's some reason buried in the code or in the minds of your teammates why it needs to be able to be nil.

Creating a status attribute for a model

I want to create a status attribute for my Taskmodel that would indicate where it is in a three part progress in this order: open => in-progress => complete. It would work in a way similar to how an Amazon package is delivered: ordered => shipped => delivered. I was wondering what would be the best way to setup this attribute. I may be wrong but creating three separate boolean attributes seemed sort've redundant. What's the best way to accomplish this?
Rails 4 has an built in enum macro. It uses a single integer column and maps to a list of keys.
class Order
enum status: [:ordered, :shipped, :delivered]
end
The maps the statuses as so: { ordered: 0, shipped: 1, delivered: 2}
It also creates scopes and "interrogation methods".
order.shipped?
Order.delivered.all
It will also map the enum values when writing queries with hash arguments:
Order.where(status: [:shipped, :delivered])
You should use the aasm gem. It has aasm_states for models, callback functionality etc.

Resources