Skip to content

Instantly share code, notes, and snippets.

@v-kolesnikov
Last active August 13, 2018 20:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save v-kolesnikov/bcbb0de809083be7cd50bba5914193ef to your computer and use it in GitHub Desktop.
Save v-kolesnikov/bcbb0de809083be7cd50bba5914193ef to your computer and use it in GitHub Desktop.

ROM-SQL and serialized YAML data

New projects that use rom-db interact with existing db schemas quite often, especially with schemas that are built on top of Rails applications. And you can probably came across to a serialized YAML data here. ActiveRecord provides API to store unstructured key-value data into single database column with serialize method:

# Serialize preferences as Hash using YAML coder.
class User < ActiveRecord::Base
  serialize :preferences, Hash
end

With this model definition if you write data as:

User.create(preferences: { locale: 'RU', timezone: 'MSK' }

it produces the following data into database:

id preferences
1 ---\n:locale: RU\n:timezone: MSK\n

Then if you read this data with rom-sql you should use suitable type for the schema attribute. For the first time you'd probably choose a wrong type, because all that you have is a database schema definition:

CREATE TABLE IF NOT EXISTS "users" (
  "id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
  "preferences" varchar
);

Column preferences has type varchar which in general corresponds to ROM::SQL::Types::String type. But if you use this type it wouldn't be so useful:

class Users < ROM::Relation[:sql]
  schema do
    attribute :id, ROM::SQL::Types::Serial
    attribute :preferences, ROM::SQL::Types::String
  end
end
users.first[:preferences]
# => "---\n:locale: RU\n:timezone: MSK\n"

Of course, ROM knows nothing about YAML and perceives data as a plain text. To deal with this data as a key-value structure we have to define custom type for this attribute:

module Types
  include Dry::Types.module

  module SQL
    YAML = Types::Array | Types::Hash
    YAMLRead = YAML.constructor(&::YAML.method(:load))
    YAMLAttr = YAML.constructor(&:to_yaml).meta(read: YAMLRead)
  end
end

class Users < ROM::Relation[:sql]
  schema do
    attribute :id, ROM::SQL::Types::Serial
    attribute :preferences, ::Types::SQL::YAMLAttr
  end
end

users.first[:preferences]
# => { locale: 'RU', timezone: 'MSK' }

Conclusion

There is a way to deal with serialized YAML data with ROM. However, it is much more important to avoid store YAML data into database. YAML is a pretty human-readable format. But it is not intended to be used with a database. Consider using JSON for the same goal. The most popular modern databases such as PostgreSQL, MySQL, MariaDB and others have quiet good enough JSON support.

#!/usr/bin/env ruby
# frozen_string_literal: true
require 'bundler/inline'
gemfile(true) do
source 'https://rubygems.org'
gem 'activerecord'
gem 'dry-types', '~> 0.12.0'
gem 'rom'
gem 'rom-sql', '~> 2.5.0'
gem 'sqlite3'
group :test do
gem 'rspec'
end
end
require 'rspec/autorun'
RSpec.configure do |config|
config.color = true
config.formatter = :documentation
end
require 'active_record'
require 'benchmark'
require 'sqlite3'
RSpec.describe 'YAML serialization' do
before do
ActiveRecord::Base.establish_connection(
adapter: :sqlite3,
database: 'database.db'
)
ActiveRecord::Migration.create_table :users do |t|
t.string :preferences
end
end
after do
`rm database.db`
end
let(:create_ar_user!) do
Test::User.create(preferences: { locale: 'RU', timezone: 'MSK' })
end
describe 'AR' do
before do
module Test
class User < ActiveRecord::Base
serialize :preferences, Hash
end
end
create_ar_user!
end
let(:user) { Test::User.first }
it do
expect(user.preferences).to eq(locale: 'RU', timezone: 'MSK')
expect(user.read_attribute_before_type_cast(:preferences))
.to eq "---\n:locale: RU\n:timezone: MSK\n"
end
end
describe 'ROM' do
let(:config) do
ROM::Configuration.new(:sql, 'sqlite://database.db')
end
let(:rom) { ROM.container(config) }
let(:users) { rom.relations[:users] }
let(:user) { users.first }
before { create_ar_user! }
context 'without special type' do
before do
config.relation(:users) do
schema do
attribute :id, ROM::SQL::Types::Serial
attribute :preferences, ROM::SQL::Types::String
end
end
end
it { expect(user[:preferences]).to eq "---\n:locale: RU\n:timezone: MSK\n" }
end
context 'with special type' do
before do
module Test
module Types
include Dry::Types.module
module SQL
YAML = Types::Array | Types::Hash
YAMLRead = YAML.constructor(&::YAML.method(:load))
YAMLAttr = YAML.constructor(&:to_yaml).meta(read: YAMLRead)
end
end
end
config.relation(:users) do
schema do
attribute :id, ROM::SQL::Types::Serial
attribute :preferences, Test::Types::SQL::YAMLAttr
end
end
end
it { expect(user[:preferences]).to eq(locale: 'RU', timezone: 'MSK') }
it 'allows to create/update' do
users.by_pk(1).changeset(:update, preferences: { locale: 'EN' }).commit
expect(user[:preferences]).to eq(locale: 'EN')
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment