Ruby - Update jsonb array column setup with jsonb_accessor - ruby-on-rails

I have this field called data that was setup with the jsonb_accessor ruby gem like this:
jsonb_accessor :data,
line_items: [:jsonb, array: true, default: []]
This is on my Contract model. And when a Contract is created, its an empty array like this:
data: {"line_items"=>[]},
I want to add multiple hashes to this empty array. For example this:
{user: "Joe"}
Then later I may add the hash:
{user: "Jack"}
I've tried many things, with NO luck including:
new = [{user: "Joe"}].to_json
Contract.last.update_all(["line_items = line_items || ?::jsonb", new])
##
Contract.last.update(["line_items = line_items || ?::jsonb", new])
Then I tried push
Contract.last.line_items.push("line_items" => {user: "Todd"})
Then I tried merge
Contract.last.line_items = Contract.last.line_items.merge(new)
Nothing has worked.

One option would just be to set the attribute with the data you want, then persist it with #save (or #save!)
contract = Contract.create!
=> #<Contract id: 1, data: {"line_items"=>[]}, line_items: []>
contract.line_items = [{foo: "bar"}]
contract.save!
contract
=> #<Contract id: 1, data: {"line_items"=>[{"foo"=>"bar"}]}, line_items: [{"foo"=>"bar"}]>
In my testing, I was able to use the #update method by just passing the object itself (rather than converting it to JSON first):
Contract.update(1, :line_items => [{foo: "bar"}])
=> #<Contract id: 1, data: {"line_items"=>[{"foo"=>"bar"}]}, line_items: [{"foo"=>"bar"}]>
Edit:
It looks like you're trying to append to the array with a single query, which appears to be possible with
Contract.where(id: 1).update_all("data=jsonb_set(data,'{line_items}', data->'line_items' || '{\"foo\": \"bar\"}')")
Though I'm sure there is a better way to achieve the same functionality...

jsonb_accessor means you don't have to manually mess around with JSON for simple accessor operations. If you want to assign to line_items use update! like normal.
contract.update!(line_items: [{user: "Joe"}])
If you want to append to a single Contract's line_items, add to the array in Ruby and update.
contract = Contract.last
line_items = contract.line_items + [{user: "Joe"}]
contract.update!(line_items: line_items)
If you want to do it all in SQL jsonb_accessor offers you no help. You have to write the SQL yourself. To add to an array, use jsonb_insert.
Contract
.where(...)
.update_all(
q[jsonb_insert(data, '{"line_items", -1}', '{user: "Joe"}', true)]
)
'{"line_items", -1}' says to insert at the last element of the key "line_items" and true has to insert after that point.

Related

Retrieving data in a hash

I need to get some data from ActiveRecord, I have following two tables Department and users I have issue that I am getting one hash in which user is giving me user_ids and emails, now I want to create hash container users, departments and emails in specific format. I have tried a lot map/select but still could not figure out any simple way.
class User < ApplicationRecord
belongs_to :department
end
And Department
class Department < ApplicationRecord
has_many :users
end
I am getting following values from user
sample_params = [{ user_id: 1, email: 'example1#example.com' },
{ user_id: 5, email: 'example5#example.com' },
{ user_id: 13, email: 'example13#example.com'}]
Now I have retrieve departments from database and other data and join in one huge hash so I can add it to my class. I can get all my users by following command
users = User.where(id: sample_params.map{ |m| m[:user_id] })
I will get whole ActiveRecord objects with all users if I run following command I am getting all user_id and project_id
users.map { |u| {user_id: u.id, department_id: u.department_id } }
I will get this
[{:user_id=>1, :department_id=>3},
{:user_id=>5, :department_id=>3},
{:user_id=>13, :department_id=>2}]
but I want to create following hash, Is there any simple way to do it directly using query or in other few lines, I can try using Iteration but that would be very long and complicated. As I also need to merge emails in it and add one project instead of same projects multiple ids.
[
{department_id: 1, users: [{user_id: 1, email: 'example1#example.com'},
{user_id: 5, email: 'example5#example.com'}]},
{department_id: 2, users: [{ user_id: 13, email: 'example13#example.com']
I am using here sample data the real data is very very large include hundreds of users and many departments.
I don't think you can do it in one go! let us run throgh and try how can we solve it. First instead of using map in your params let us try another alternate for it. Remove following line
users = User.where(id: sample_params.map{ |m| m[:user_id] })
user following line
User.where(id: sample_params.pluck(:user_id)).pluck(:department_id, :id).grou
p_by(&:first)
This will bring you result in each users id with department grouping in one query
{3=>[[3, 1], [3, 5]], 2=>[[2, 13]]}
Now we are getting department_id and user_id so we will run map on them to get first array with department_id and user_id in a group with following command
data =
User
.where(id: sample_params.pluck(:user_id))
.pluck(:department_id, :id)
.group_by(&:first).map do |department_id, users|
{ department_id: department_id,
users: users.map { |_, id| id } }
end
This will give you hash with department and users
[{:department_id=>3, :users=>[1, 5]}, {:department_id=>2, :users=>[13]}]
Now you have hash of department and users. so let us work on this this is second phase where I will use select to get email from your params and merge it to your data.
result = []
data.each do |department|
department_users = []
department[:users].each do |user|
emails = sample_params.select { |user| user[:user_id] == 1 }[0][:email];
department_users << { id: user, emails: emails }
end; result << {department_id: department[:department_id], users: department_users}
end
Now if you do puts result[0] you will get following hash as you wanted.
{:department_id=>3, :users=>[{:id=>1, :emails=>"example1#example.com"}, {:id=>5, :emails=>"example1#example.com"}]}, {:department_id=>2, :users=>[{:id=>13, :emails=>"example1#example.com"}]}
This will solve issue, I understand there are two operation but there is no double sql queries in single it is working and you are also replacing your emails. I hope it solve you issue, any one can make it simpler would be appreciated.

Unable to get the values of enum in database

In rails 4, I have created an enum called books which is
enum books_category: {
horror: 1,
mystery: 2,
drama: 3
}
I have a column in the database called Books where I populated it using a csv file. My database attributes are name and books_category. Now when I upgraded to rails 5, books_category attribute of database is directly mapping to the string instead of integer. For example, previously books_category consists of 1 after upgrading it's saving the string "horror". How do I solve this issue?
You can use two ways.
1) create a model which saves business_categories in it.
or
2) when you retrieve record which is storing horror, you can pass it to the model like
my_model = Model.find_by("business_categot = ?","horror")
Model.books_category[my_model.books_category] # Returns the integer value
First of all, you are missing a colon after books_category. The code should look like:
enum books_category: {
horror: 1,
mystery: 2,
drama: 3
}
Please edit your question to include the datatype of books_category attribute.
You can change the value either using strings or ints
book = Book.last
book.books_category = 1
book.save!
=> book.books_category = "horror"
book = Book.last
book.books_category = 'drama'
book.save!
=> book.books_category = "drama"
Please note that Rails always returns the string value even though they are saved in integer datatype.
You can get the hash of all books_categories by calling
Book.book_categories
=> {"horror"=>1, "mystery"=>2, "drama"=>3}
And the keys and values by calling
Book.book_categories.keys
=> ["horror", "mystery", "drama"]
and
Book.book_categories.values
=> [1, 2, 3]
Finally, you can get a list of all the objects(books) having the category of horror with
Book.horror
=> Book Load (0.1ms) SELECT "books".* FROM "books" WHERE "books"."books_category" = ? [["books_category", 1]]
As you can observe from the query, it uses integer to search through the Books.
If you really want the values of your enum, consider using Enumerize gem. It is simpler and offers much more flexibility
Further Reading: https://hackhands.com/ruby-on-enums-queries-and-rails-4-1/

Rails 4 - How to select a subset of columns, with column names, not including id? Select or Pluck?

Say I have a model, Bean, and I have the following query:
sqlParams = {}
sqlQuery = "overall_rating >= :rating"
sqlParams[:rating] = #rating
Bean.where(sqlQuery, sqlParams).select("name")
then I get:
#<Bean id: nil, name: "Kenya AA Wagamuga Auction Lot">
However, if I use:
Bean.where(sqlQuery, sqlParams).pluck("name")
then I get:
"Kenya AA Wagamuga Auction Lot"
What I want is this:
name: "Kenya AA Wagamuga Auction Lot"
I don't want that nil id, but I do want the column name. What's the best way to go about getting this sort of response? I'm passing it straight into a hash and then to a json for an http response. Keep in mind that I'm just ramping up on Rails, so let me know if I'm being silly. Thanks!
I think it doesn't hurt to build the desired hash yourself.
Bean.where("overall_rating >= ?", rating).pluck('name').map do |elem|
{'name' => elem}
end
Or, especially if you'll need more attributes, you can try this:
Bean.where("overall_rating >= ?", rating).map do |elem|
elem.serializable_hash.slice('name', ...)
end
So I ended up using select, then filtering out the attributes I didn't want via map. I'm not sure the fastest solution, but this one seemed clean to me.
Thanks all for the input!
json_return[:reviews] = Bean.where(sqlQuery, sqlParams).limit(#count).reorder(#order_string).as_json.map do |bean|
bean.except("id", "created_at", "updated_at")
end

List reference columns of a active record model

using Rails 3.2, there is a way to find out if a column is a reference column to other model?
I don't want to rely in "_id" string search in the name.
thanks.
UPDATE:
I need to iterate over all columns and made a special treatment in references columns, something like:
result = Hash.new
self.attribute_names.each do |name|
if self[name]
result[name] = self[name]
if name is reference column
insert xxx_description (from the other model) in hash.
end
end
end
I will return this hash as json to client.
{name: 'joseph', sector_id: 2, sector_name: 'backend'...}
Where sector_name, is person.sector.name...
thanks.
alternative method if you don't know the name of the association :
Post.reflect_on_all_associations(:belongs_to).map &:foreign_key
# => ['author_id','category_id']
See http://api.rubyonrails.org/classes/ActiveRecord/Reflection/ClassMethods.html
Post.reflections[:comments].primary_key_name # => "message_id"
How to get activerecord associations via reflection

Array manipulation (summing one column by certain group, keeping others the same)

I have a table of Logs with the columns name, duration, type, ref_id.
I update the table every so often so perhaps it will look like a col of ['bill', 'bob', 'bob', 'jill'] for names, and [3, 5, 6, 2] for duration, and ['man', boy', 'boy', 'girl'] for type, and [1, 2, 2, 3] for ref_id.
I would like to manipulate my table so that I can add all the durations so that I get a hash or something that looks like this:
{'name' => ['bill', 'bob', 'jill'], 'duration' => [3, 11, 2], 'type' => ['man', 'boy', 'girl'], ref_id => [1, 2, 3]}
How can I do this?
(for more info--currently I'm doing Log.sum(:duration, :group => 'name') which gives me the names themselves as the keys (bill, bob, jill) instead of the column name, with the correct duration sums as their values (3, 11, 2). but then I lose the rest of the data...)
I guess I could manually go through each log and add the name/type/ref_id if it's not in the hash, then add onto the duration. If so what's the best way to do that?
If you know of good sources on rails array manipulation/commonly used idioms, that would be great too!
Couple of notes first.
Your table is not properly normalized. You should split this table into (at least) two: users, and durations. You should do this for lots of reasons, that's relational databases 101.
Also, the hash you want as a result also doesn't look right, it suggests that you are pre-grouping data to suit your presentation. It's usually more logical to put these results in an array of hashes, than in a hash of arrays.
Now on to the answer:
With your table, you can simply do GROUP BY:
SELECT name, type, ref_id, SUM(duration) as duration
FROM logs
GROUP BY name, type, ref_id
or, using AR:
durations = Log.find(:all,
:select => 'name, type, ref_id, SUM(duration) as duration',
:group => 'name, type, ref_id'
)
In order to convert this to a hash of arrays, you'd use something like:
Hash[
%w{name, type, ref_id, duration}.map{|f|
[f, durations.map{|h|
h.attributes[f]
}]
}
]
Maybe all you need is something like this that spins through all the log entries and collects the results:
# Define attributes we're interested in
operate_on = %w[ name duration type ref_id ]
# Create a new hash with placeholder hashes to collect instances
summary = Hash[operate_on.map { |k| [ k, { } ] }]
Log.all.collect do |log|
operate_on.each do |attr|
# Flag this attribute/value pair as having been seen
summary[attr][log.send(attr)] = true
end
end
# Extract only the keys, return these as a hash
summary = Hash[summary.map { |key, value| [ key, value.keys ] }]
A more efficient method would be to do this as several SELECT DISTINCT(x) calls instead of instancing so many models.
Didn't quite understand if you want to save records from your hash, or you want to query the table and get results back in this form. If you want to get a hash back, then this should work:
Log.all.inject({}) do |hash, l|
hash['name'] ||= []
hash['duration'] ||= []
hash['type'] ||= []
hash['ref_id'] ||= []
hash['name'] << l.name
hash['duration'] << l.duration
hash['type'] << l.type
hash['ref_id'] << l.ref_id
hash
end

Resources