I have some HStore columns in my Rails app. Looking around the Posgresql documentation and some blog posts, I've discovered that, given an HStore value that is a hash like this:
{ some_key: 'value' }
I can query the columns like so:
Model.where("the_hstore_column -> 'some_key' = 'value'")
There are a bunch of issues with this as a sufficient querying tool, however:
It really only makes sense for super simple values. If the value is itself a hash or something, I have no idea how to search for it effectively, if I don't know all its contents. (Even if I did, I'd have to turn them all to stringified JSON or something).
It isn't helpful (at least, I can't make it helpful) for doing queries for the presence or non-presence of the column's content (or the content of any key under the column).
In other words (in pseudo-sql), I'd love to be able to do this:
Model.where("the_hstore_column = {}")
Model.where.not("the_hstore_column = {}")
Or:
Model.where("the_hstore_column -> 'some_key' = NULL")
Model.where.not("the_hstore_column -> 'some_key' = NULL")
Or best yet, given an HStore of value { some_key: { sub_key: 'value' } }
:
Model.where("the_hstore_column -> 'some_key' INCLUDES (?)", 'sub_key')
Model.where.not("the_hstore_column -> 'some_key' INCLUDES (?)", 'sub_key')
These appear not to be working, but for the life of me, I can't find great information on how to conduct these queries. Does anyone know how to write them, or where I could look for better information?
UPDATE After more looking around, I found this post, which looked promising, but I can't get the code to actually work. For example Model.where("the_hstore_column ? :key", key: 'some_key')
is returning an empty relation, even if there are many Model
objects with some_key
in the_hstore_column
.
As requested by Andrius Buivydas, I'm pasting the relevant portion of my model below. It's relatively brief, because I decided to abstract out the Hstore-accessing into a module I wrote, essentially turning it into a hash store (which I though, apparently incorrectly, was its whole purpose). I tried using the built-in store_accessor
, but it didn't work to my liking, (didn't seem to help parse saved hashes, for now obvious-seeming reasons) thus the ActiveRecord::MetaExt::HstoreAccessor
module below. (ParsedHstore
just takes an HStore string and parses it into a hash).
class Place.rb
include ActiveRecord::MetaExt::HstoreAccessor
hstore_accessor :hours, :extra
end
(In a separate file):
module ActiveRecord
module MetaExt
module HstoreAccessor
module ClassMethods
def hstore_accessor(*symbols)
symbols.each do |field|
class_eval do
define_method(field) do
ParsedHstore.new(self[field.to_sym]).hash_value
end
define_method("add_to_#{field}!") do |arg|
self[field.to_sym] = self[field.to_sym].merge(arg)
send("#{field}_will_change!")
save
arg
end
define_method("add_to_#{field}") do |arg|
self[field.to_sym] = self[field.to_sym].merge(arg)
send("#{field}_will_change!")
arg
end
define_method("remove_from_#{field}") do |arg|
other = self[field].dup
other.delete(arg.to_s); other.delete(arg.to_sym)
self[field.to_sym] = other
send("#{field}_will_change!")
self[field.to_sym]
end
define_method("remove_from_#{field}!") do |arg|
other = self[field].dup
other.delete(arg.to_s); other.delete(arg.to_sym)
self[field.to_sym] = other
send("#{field}_will_change!")
save
self[field.to_sym]
end
define_method("set_#{field}") do |arg|
self[field.to_sym] = arg
send("#{field}_will_change!")
self[field.to_sym]
end
define_method("set_#{field}!") do |arg|
self[field.to_sym] = arg
send("#{field}_will_change!")
self[field.to_sym]
end
end
end
end
end
def self.included(base)
base.extend ClassMethods
end
end
end
end
The idea is that this lets me easily add/remove values to an HStore field, without having to think about the merging and _will_change!
logic every time. So I could do this, for example: Place.first.add_to_extra({ key: 'value'})
.
Should I have made these fields
json
orjsonb
? I feel like I'm reinventing the wheel here, or, more aptly, trying to turn a horse into a wheel or something.
Also, I may be misunderstanding the query example. I literally tried this query in my database (which has many places with a non-empty ranking
key under the extra
field), and turned up with an empty relation:
Place.where("extra ? :key", key: 'ranking')
I wouldn't be surprised if I messed this syntax up, as it seems really strange. Wouldn't that replace the
?
with'ranking'
, turning the query into this?:Place.where("extra ranking :key")
? Seems weird and emphatically different from any other SQL I've run. Or is it turning toPlace.where("extra ? ranking")
? But?
is usually for safe injection of variables, no?
Please let me know if something else in my model or elsewhere would be more relevant.
It really only makes sense for super simple values. If the value is itself a hash or something, I have no idea how to search for it effectively, if I don't know all its contents.
Postgresql HStore stores key, values pairs that are both strings, only. So you can't store a hash, a nil or something else like an object - they will be converted to the strings.
Model.where("the_hstore_column ? :key", key: 'some_key')
That should work if everything is defined correctly. Could you paste an extract of the model file with the definition of the hstore column values?