Rails and Postgres, group by hstore - ruby-on-rails

I have an 'Audit' model with name:string and data:hstore attributes - as follows:
#<Audit
id: 1,
name: "my audit",
data: {"publisher"=>"some publisher", "display_version"=>"17.012.20098"},
created_at: "2017-10-10 13:09:56",
updated_at: "2017-10-10 13:09:56">
I want to produce a report that contains 3 columns:
name, display_version, count
To achieve this I assume that I need to group by "name" and "data -> 'display_version'"
I am struggling with the query and AR syntax.
What I have so far is:
Parent.audits.group(:name, :data -> 'display_version)'
Even this doesn't work. Can someone help? I have searched for examples without any luck.

You can pass a string containing the SQL grouping expression to group so you could say:
Parent.audits.group(%q{name, data -> 'display_version'})
or:
Parent.audits.group(%q{audits.name, audits.data -> 'display_version'})
And then you could throw a #count call on the end to get the data you want:
Parent.audits.group(%q{name, data -> 'display_version'}).count

Use jsonb instead of hstore (for a variety of reasons)
'Audit' model with name:string and data:jsonb
Audit.rb
store_accessor :data, :publisher, :display_version
Then try:
Parent.audits.pluck(:name, :publisher, :display_version)
Good Resource on JSONB and Postgresql
hstore columns don’t allow a nested structure; they also store all values as strings, which will require type coercion on both database and application layers. With json/jsonb columns you don’t have this problem; numbers (integers/float), booleans, arrays, strings and nulls are accepted, and you can even nest structures in any way you want.
The recommendation is that you stop using hstore in favor of jsonb;
just remember that you have to use PostgreSQL 9.4+ for this to work.

Related

Rails: How to get enum value for joined table?

I have two models with enum fields:
class TempAsset < ApplicationRecord
enum state: { running: 0, stopped: 1, terminated: 2 }
end
class AssetCredential < ApplicationRecord
enum map_status: { pending: 0, inprogress: 1, passed: 2, failed: 3 }
end
When I select column from the first table, it gives proper values from enum:
TempAsset
.joins('INNER JOIN asset_credentials
ON temp_assets.instance_id = asset_credentials.instance_id')
.pluck(:state)
.uniq
# ["stopped", "running"]
But, it gives numbers when I select column from the joined table:
TempAsset
.joins('INNER JOIN asset_credentials
ON temp_assets.instance_id = asset_credentials.instance_id')
.pluck(:map_status)
.uniq
# [0, 3, 2, 1]
So, should I do something like this:
AssetCredential.map_statuses.key(0) => "pending"
AssetCredential.map_statuses.key(1) => "inprogress"
Or is there any better way to do the same?
ActiveRecord::Enum is not designed to work across join table, therefore you cannot select or pluck other table enum column and expect the mapping value in return. As you said what you can do is to pluck the integer value and do the mapping by yourself.
Or in my case, I use enumerize gem which stores values in the database and gives you more options and customization such as validation and I18n. With this gem you can use your code above to pluck the expected values (because it stores exact value not mapping with integer).
See, the problem here is you are using active-record enums. These are stored as integers in the database, and mapping to some text is done on the application level. pluck works on the db level.
Whats happening here is when you do a inner join, it is collection columns from both tables. When you pluck from the table which you are joining-from, active record knows how to map it to the enum. In the second one, you are plucking column from another table, and active-record is unable to get the context.
Here, you can try using postgres enums for proper results.
TempAsset
.joins('
JOIN asset_credentials
ON temp_assets.instance_id = asset_credentials.instance_id
')
.where(
state: TempAsset.states[:terminated],
asset_credentials: { map_status: AssetCredentials.map_statuses[:passed] }
)
You can change the keys being passed in to whatever you need. I'm not sure what model you want returned but your could switch things around to suit your needs
I tried to simulate your problem. The interesting thing which I found here was this issue does not arise when we use 'joins' on 'one-to-many' relationship.
i.e., if one 'TempAsset' has many 'AssetCredentials', the query
TempAsset.joins(:asset_credentials)
gives out enum values as you expect. Whereas
AssetCredential.joins(:temp_asset) does produce the issue which you have mentioned.
So, If you can establish 'has_many' relationship between 'TempAsset' and 'AssetCredentials' through a 'has_one' relationship between 'TempAsset' and 'Instance', the issue could get fixed.
Given the rails associations are setup, can also try something like:
TempAsset
.joins('join asset_credentials on temp_assets.instance_id = asset_credentials.instance_id')
.map { |temp_asset| [temp_asset| [temp_asset.asset_credential.map_status] }
.uniq
Try this:
AssetCredential.joins('INNER JOIN temp_assets ON asset_credentials.instance_id = temp_assets.instance_id').pluck(:map_status).uniq

Rails Postgres filtering for a jsonb object inside of a jsonb array

Let's say I have a model Neighborhood that has a jsonb[] field families, which is an array containing json objects with any type of key value pairing like so [{"name":"Smiths", "count":4}, {"name":"Miller","out_on_vacation":false}, {"name":"Bennet", "house_color":"red", "count": 4}]
I want to do an activerecord query to find Neighborhoods for Neighborhoods having certain objects inside their families array.
So if I did something like Neighborhood.where({families: {count: 4}), the result would be any Neighborhood models whose families field contain a jsonb object with a key value pairing of count: 4. I've played around with a bunch of different queries, but can't seem to get any of them to work without getting an error back. How would I go about writing an Activerecord query to getthe desired results?
EDIT:
I had run a migration like so:
def change
add_column :neighborhoods, :families, :jsonb, array: true, default: [], index: true
end
I believe you would do something like:
Neighborhood.where("families -> 'count' ? 4")
This article might help you: http://nandovieira.com/using-postgresql-and-jsonb-with-ruby-on-rails
edit: Just noticed that you have an array inside of the jsonb, so this probably won't work.
edit 2: This was answered over on Reddit and worked for me as well. Answering here as a reference for myself.
Neighborhood.where %q(families #> '[{"count":?}]'), 4

Rails 4 - How to store an array on postgres

I need to save this array:
params[:products]
The array contains these information:
[{"'name'"=>"Product Name 1 ", "'id'"=>"2", "'quantity'"=>"2", "'accessory'"=>{"'id'"=>"8", "'name'"=>"Accessory Name 1"}}, {"'name'"=>"Product Name 2 ", "'id'"=>"5", "'quantity'"=>"1", "'accessory'"=>{"'id'"=>"40", "'name'"=>"Accessory Name 2"}}]
As you can see, accessory is another array.
The process is this: A front-end guy is givin me that array, So I want to store all data on order.rb model.
So, I have a couple of questions:
Do I need to have "array type field" on database?.
Which fields do I need?
I was looking for some examples and I've been trying this on my order model:
serialize :product
order = Order.new
order.product = [:product]
order.save
order.product
I red about this method too: http://api.rubyonrails.org/classes/ActiveRecord/Store.html
Maybe this is a basic question but I really don't know how to solve it. As you can see, I don't have code in any controller because I really don't know what I need to write.
Thank you for your help.
Besides hstore, another solution would be JSON, specifically I suggest you use the PostgreSQL type "jsonb" because it's more efficient, see the documentation:
There are two JSON data types: json and jsonb. They accept almost identical sets of values as input. The major practical difference is one of efficiency. The json data type stores an exact copy of the input text, which processing functions must reparse on each execution; while jsonb data is stored in a decomposed binary format that makes it slightly slower to input due to added conversion overhead, but significantly faster to process, since no reparsing is needed. jsonb also supports indexing, which can be a significant advantage.
(more info here https://www.postgresql.org/docs/current/static/datatype-json.html )
So you have, similarly to hstore, a data structure that you can execute queries against, and this queries are quite fast as you can read above. This is a significant advantage over other strategies, e.g. serializing a Ruby hash and saving it directly in the DB.
Charles,
I suggest you to consider using hstore type of your postgres. There are few benefits of using it (performance, indexing of objects etc..).
enable_extension 'hstore'
This actually enables your PSQL have this support.
Your migration is going to be like this:
class AddRecommendationsToPages < ActiveRecord::Migration
def change
add_column :pages, :recommendations, :hstore
end
end
And after that you can pass into your hstore anything you want..
irb(main):020:0> Page.last.recommendations
Page Load (0.8ms) SELECT "pages".* FROM "pages" ORDER BY "pages"."id" DESC LIMIT 1
=> {"products"=>"34,32"}
irb(main):021:0> Page
=> Page(id: integer, block: string, name: string, slug: string, title: string, h1: string, annotation: text, article: text, created_at: datetime, updated_at: datetime, position: integer, parent: integer, show: boolean, recommendations: hstore)
irb(main):022:0> Page.last.recommendations["products"]
Page Load (0.6ms) SELECT "pages".* FROM "pages" ORDER BY "pages"."id" DESC LIMIT 1
=> "34,32"
irb(main):023:0>

Mapping and converting column names to variables in json format in ruby

I am fetching values from a database table (client). I am interested in two columns (first_name and id). Since I am sending the response as a json format, I would like to achieve a result like this
students: ["first_name": "adam", id:"12", ....]
I tried Student.all.map{|s| [s.first_name, s.id]} but here I just get the values(adam, 12) not the indexes ("first_name", "id")
is there a way to make the column name appear in the response without complicating the query
Less magic, then mohamed's answer, just using basic tools:
#students.map{|st| {id: st.id, first_name: st.first_name}}
You can use only with json:
Student.all.to_json(:only=>[:id, :first_name])
That will work if you not overriding as_json or to_json in student model

Custom query with Squeel on equality of two attributes to ordered pairs

I'm having difficulty writing a query, whether in SQL or not, but I'd like to be able to write it in ruby using the gem Squeel. Basically, I have a class:
class Article < ActiveRecord::Base
belongs_to :genre
belongs_to :publisher
...
end
I want a scope that takes in an array of ordered pairs of the format (genre_id, publisher_id) and outputs an ActiveRecord::Relation that contains all of the records with genre, publisher pairs equal to the pairs passed in.
Basically, I want Article.where{ genre_id.eq_any [1, 5, 76] }, except instead of a single attribute, I want it on a pair of attributes:
Article.where{ genre_id_publisher_id.eq_any [(1,2), (1,4), (2, 4)] }
The only way I can think of doing it is making a scope with a select call that adds a column which would be the concatenation of the two ids with some delimiter, then searching based on that:
Article.select(*,
"#{genre_id}-#{publisher_id}" as genre_publisher_pair).where(
genre_publisher_pair.eq_any ["1-2", "1-4", "2-4"])
However, that seems like too much overhead. I also already have a compound index on [genre_id, publisher_id] and I'm afraid this won't use that index, which is going to be a requirement for me.
Is there an easier way to write these compound scopes? Thanks!

Resources