Ask A Question

Notifications

You’re not receiving notifications from this thread.

Preferences & Settings with ActiveRecord::Store Discussion

Great stuff

Reply

I created this for Rails 4 https://rubygems.org/gems/h... using JSON columns. It is the same idea.

I say that purely to vouch for this method of prefs/settings storage.

Reply

This is awesome, thanks for sharing that John!

Reply

you're saying we can query items inside the Preferences attribute.

in other words..
User.where(playback_rate: "0.75")

...would work?

Reply

You can as long as you're using hstore or json column in Postgres, but it's not quite the normal where syntax like you mentioned. You have to use the SQL syntax I showed in the episode for querying JSON columns (note the "->>" part at the end).

Hstore has its own syntax as well that's a bit different and uses "@>" and some other syntax.

Reply

Great video Chris. Pro's / Con's JSONB vs JSON pure PG column? I know Codeship had a good write-up on the two ( https://blog.codeship.com/u... ), but was interested in anything you had on that front? Cheers, Nick

Reply

I believe that JSONB is probably the best format for this because it's a binary representation. Should be more efficient for most everything. There's some more information on this stuff here: https://www.citusdata.com/b...

Reply

I've fallen into using this pattern several times, but I'd very down on it now. It's usually because I've been inappropriately afraid of column or migration proliferation, but that's actually a false alarm. PostgreSQL can handle hundreds of columns, and migrations aren't that scary.

Each time, I've ended up with an awful lot of internal infrastructure and un-rails-y configuration-over-convention (like those typed stores) to reinvent something that PostgreSQL, in conjunction with ActiveRecord, does exceptionally well by design: storing typed scalars. The code smell is compounded by heaps of nil-whacking and existence-checking and validations which using a simple DB column would make wholly unnecessary.

The most shameful case is boolean attributes, which no longer have two states to deal with, but five: true, false, nil, nonexistent, and oops a string. Yes, that last cropped up for me because some other app used a string during an import. When you see code handling a five-state boolean, you a) have a little cry, and then b) deal with the stench.

For my major app, I recently threw away the JSON preferences store and made them all regular columns. The diff is mostly lines removed.

I still use JSON stores, but reserved for three cases:
1. In which there's an application-level semantic difference between attribute having the null value, and attribute not present. In this case I find using the JSON stores are less hassle than implementing the EAV pattern.
2. In which there is a deep JSON structure to store & query e.g. when we have user-supplied attribute names (in which case we're not using store_accessors anyway)
3. When I have more columns than PostgreSQL can handle. (This has never happened)

I will never willingly use them as a first choice for settings/preferences again.

Reply

This actually makes a lot of sense. I've had frustrations with putting all my preferences in a single column. It bothers me to have a bunch of columns for preference data, but as I think about it, there's no "real" reason it should bother me; it's my own "preference" (no pun intended). I would have access to all of ActiveRecord's power, and the data would be treated like a first-class citizen instead of delegated to the sidelines. You're right that migrations aren't something to be scared about, and if you use a single column hash you have to re-deploy changes anyway, which should run migrations by default.

Adding 10-15 DB columns for preferences may be a better solution, because then you have full index/query power, even though JSONB can be indexed (I think). No reason to complicate things though, IMO.

One use case I've had for using single-column hashes is for unstructured data that you don't know ahead of time, such as someone uploads a JSON file with unknown data and you have to parse it out. But preferences are 99% of the time structured data that you know ahead of time and are building core logic around (ie, "send emails if this setting is true").

Reply

You can definitely do all of that with typed store and a json column. Like I showed in the example, it casts both going in and out of the store, so there's no chance you'll have booleans with 5 states.

One better example is probably feature flags that will often come and go. Adding and removing columns for every feature flag is going to be way overkill for something that can easily be handled through a store.

Reply
Michael Tao Michael Tao

Great topic Chris!

Would you recommend this method if I want to store a lot of user information? Say I'm building a digital resume, experience, skills (tags), educations etc.. My end goal is to able to search this data either with angularjs or searchkick. Currently I have several tables user, user_jobs, user_experiences, user_skills etc.

Reply

I did find one "gotcha" with using a preferences hash like this. In my particular situation, I wanted a settings hash that stored a nested filters hash.

My use case is I have a page where I want to use URL params for filtering:

/contacts?first_name=John&last_name=Smith

I wanted to store the filters hash via:

# in controller
user.update_attribute(settings[:contact_filters], params.slice(:first_name, :last_name))

# What I want it to do...
settings: {
contact_filters: {
first_name: "John",
last_name: "Smith"
}
}

The problem is, that the nested hashes are stored as strings and it is nigh impossible to cast them cleanly to a normal Ruby hash (at least after 15 min of StackOverflow searching).

I was thinking that I could get it to somehow work with a combination of:

JSON.parse(user.settings[:contact_filters])

but no such luck. I do love the simplicity of the nested hashes, but it does seem to have some difficulties at least with nested hashes.

Reply
Hey Chris, is there a way to use the Rails 5+ attributes API to store settings in a jsonb column instead of using the activerecord-typedstore gem?  Trying to see if I can remove external dependencies and use built-in Rails features.
Reply

There is a gem for this (like always with rails) that uses the attributes API: https://github.com/DmitryTsepelev/store_model

Reply

I've just been looking to add this to my app on a User model. Couple of questions:

  1. Can nested data be stored / accessed? If not what would be a better way to handle that?
  2. Does SQLite & MySQL support the JSON column type?
Reply

For anyone who may be wanting to use Typed_store in rails 6, there is currently no support.
https://github.com/byroot/activerecord-typedstore/issues/64

Reply

I'm currently upgrading from Rails 5.2 to 6.0.1 and this gem was one of the blockers for me since it has a dependency on Rails 5.2. Really the only problem is that Rail's built-in store persists these values as strings and this gem typecasts the values for you. Once you handle that, you don't need this gem anymore and can write your own module or small gem.

See below for working version that doesn't use the gem. Obviously I haven't addressed null values and defaults, but those are fairly simple. Once I have a working module I may update this comment with the code so others can use it.

I don't think it's worth making a gem to add a few lines of code, so I tend to store something like this in a lib/modules/typed_store.rb file and then include TypedStore in my model file to use it.

Code from the typed_store gem (from my live project)

typed_store :recurring_rules, coder: DumbCoder do |s|
    s.integer :recurring_interval, default: 1, null: true
    s.string :recurring_frequency, default: "day", null: true
    s.integer :recurring_days, array: true, default: nil, null: true
    s.integer :recurring_day_of_month, default: nil, null: true
  end

Using built-in Rails store

If you're using PG with jsonb column types, you can use store_accessor directly and don't have to use the store method.

Just override the accessor methods to handle typecasting; that's also where you would handle defaults, nulls, etc...

store_accessor :recurring_rules,
    :recurring_interval,
    :recurring_frequency,
    :recurring_days,
    :recurring_day_of_month

  def recurring_interval
    super.to_i
  end

  def recurring_frequency
    super.to_s
  end

  def recurring_days
    super.to_a.map(&:to_i)
  end

  def recurring_day_of_month
    super.to_i
  end
Reply

Can anybody point to some resources of using this with an array in a jsonB column

So For eg, if data was like this:

 equipment_required :jsonb

> Report.first.equipment_required
=> 
[
{"name"=>"Brushwood Chipping", "size"=>"6\"", "type"=>"Wheeled", "quantity"=>"1", "hours_required"=>"2"}, 
{"name"=>"Tipper", "type"=>"Normal", "quantity"=>"1", "hours_required"=>"2"}
]
Reply

Why settings is a text format and preferences is a json format ?

Reply

What is the syntax for querying a boolean value?

Trying the below doesn't seem to work? and throws the following error:

PG::UndefinedFunction: ERROR: operator does not exist: text = boolean

current_user.companies.where("holiday_request_settings ->> :key = :value", key: "enable_holiday_requests", value: true)

If I try the following I get no results:

current_user.companies.where("holiday_request_settings ->> :key = :value", key: "enable_holiday_requests", value: "true")

The company current has the following data:

holiday_request_settings: {"enable_holiday_requests"=>true, "holiday_allowance_days"=>20}

This is the code on the model:

typed_store :holiday_request_settings, coder: JSON do |s|
        s.boolean :enable_holiday_requests, default: false
        s.integer :holiday_allowance_days
end

Any ideas?

Reply

After struggling a lot with JSONB, Rails 6 and storeext (https://github.com/G5/storext) and the typedstore Gem mentioned here I finally found the following one and it works flawlessly. Maybe it's helpful for someone else: https://github.com/madeintandem/jsonb_accessor

My problem was always that the JSON in the column was escaped / stored as a string, which it shouldn't be in a JSONB column. After switching to this Gem everything worked, I'm not exactly sure where it's going wrong.

Reply

I am using this gem and it's been going great, but after implementing it I ran my tests and they all fail with the message:
Jumpstart::AccountsTest::AdminUsers#test_can_edit_account:
NoMethodError: undefined method jsonb_accessor' for #<Class:0x00007f84226d5dc8>
app/models/account.rb:51:in
class:Account'
app/models/account.rb:29:in `

'

Any ideas on why might be causing this or how to fix it? Thanks!

Reply

Various gems out there to tackle this issue:

@Chris: I've been working with some of these gems before, but I haven't quit found out which one I like best. I'd love to see a video cast on working with jsonB (as at first it can be a bit confusing) and probably an overview of these Gems. Which one do you think works best?

Reply

I followed this episode and I tried doing this

  typed_store :meta, coder: ActiveRecord::TypedStore::IdentityCoder do |s|
    s.string :requested_topics
    s.text :instructions
  end

s.string works but s.text gives me the error key not found: :text even though its listed here

[:string, :text, :integer, :float, :time, :datetime, :date, :boolean, :decimal, :any].each do |type|

does anyone know how to use a text field with typed_store?

Reply
Join the discussion
Create an account Log in

Want to stay up-to-date with Ruby on Rails?

Join 82,584+ developers who get early access to new tutorials, screencasts, articles, and more.

    We care about the protection of your data. Read our Privacy Policy.