PgSerializable
Description
Serialize json directly from postgres (9.4+).
Upgrading from version 2.x.x to 3.x.x
Serialization returns a PORO instead of a json string. If you have code like:
OJ.load(Product.where(id: ids).json)You can replace it with:
Product.where(id: ids).jsonTo automatically use OJ to serialize your POROs to json strings, add this code to your initialization:
Oj.optimize_rails()Motivation
Models:
class Product < ApplicationRecord
has_many :categories_products
has_many :categories, through: :categories_products
has_many :variations
belongs_to :label
end
class Variation < ApplicationRecord
belongs_to :product
belongs_to :color
end
class Color < ApplicationRecord
has_many :variations
end
class Label < ApplicationRecord
has_many :products
end
class Category < ApplicationRecord
has_many :categories_products
has_many :products, through: :categories_products
endUsing Jbuilder+ActiveRecord:
class Api::ProductsController < ApplicationController
def index
@products = Product.limit(200)
.order(updated_at: :desc)
.includes(:categories, :label, variations: :color)
render 'api/products/index.json.jbuilder'
end
endCompleted 200 OK in 2975ms (Views: 2944.2ms | ActiveRecord: 29.9ms)Using fast_jsonapi:
class Api::ProductsController < ApplicationController
def index
@products = Product.limit(200)
.order(updated_at: :desc)
.includes(:categories, :label, variations: :color)
options = {
include: [:categories, :variations, :label, :'variations.color']
}
render json: ProductSerializer.new(@products, options).serialized_json
end
endCompleted 200 OK in 542ms (Views: 0.5ms | ActiveRecord: 29.0ms)Using PgSerializable:
class Api::ProductsController < ApplicationController
def index
render json: Product.limit(200).order(updated_at: :desc).json
end
endCompleted 200 OK in 54ms (Views: 0.1ms | ActiveRecord: 43.0ms)Benchmarking fast_jsonapi against pg_serializable on 100 requests:
user system total real
jbuilder 175.620000 70.750000 246.370000 (282.967300)
fast_jsonapi 37.880000 0.720000 38.600000 ( 48.234853)
pg_serializable 1.180000 0.080000 1.260000 ( 4.150280)You'll see the greatest benefits from PgSerializable for deeply nested json objects.
Installation
Add this line to your application's Gemfile:
gem 'pg_serializable'And then execute:
$ bundle
Or install it yourself as:
$ gem install pg_serializable
Configuration
To ensure traits are valid during rails initialization instead of when accessed:
# config/initializers/pg_serializable.rb
PgSerializable.validate_traits!Migrating from version 1 to 2
Trait validations were occasionally running before the class's traits were loaded when there are complex dependencies. These were moved out of the class definition. To maintain existing behavior, add PgSerializable.validate_traits! to an initializer. See configuration.
Usage
In your model:
require 'pg_serializable'
class Product < ApplicationRecord
include PgSerializable
serializable do
default do
attributes :name, :id
attribute :name, label: :test_name
end
end
endYou can also include it in your ApplicationRecord so all models will be serializable.
In your controller:
render json: Product.limit(200).order(updated_at: :desc).jsonIt works with single records:
render json: Product.find(10).jsonAttributes
List attributes:
attributes :name, :idresults in:
[
{
"id": 503,
"name": "Direct Viewer"
},
{
"id": 502,
"name": "Side Disc Bracket"
}
]Re-label individual attributes:
attributes :id
attribute :name, label: :different_name[
{
"id": 503,
"different_name": "Direct Viewer"
},
{
"id": 502,
"different_name": "Side Disc Bracket"
}
]Wrap attributes in custom sql
serializable do
default do
attributes :id
attribute :active, label: :deleted { |v| "NOT #{v}" }
end
endSELECT
COALESCE(json_agg(
json_build_object('id', a0.id, 'deleted', NOT a0.active)
), '[]'::json)
FROM (
SELECT "products".*
FROM "products"
ORDER BY "products"."updated_at" DESC
LIMIT 2
) a0[
{
"id": 503,
"deleted": false
},
{
"id": 502,
"deleted": false
}
]Traits
serializable do
default do
attributes :id, :name
end
trait :simple do
attributes :id
end
endrender json: Product.limit(10).json(trait: :simple)[
{ "id": 1 },
{ "id": 2 },
{ "id": 3 },
{ "id": 4 },
{ "id": 5 },
{ "id": 6 },
{ "id": 7 },
{ "id": 8 },
{ "id": 9 },
{ "id": 10 }
]Associations
Supported associations:
belongs_tohas_manyhas_many :throughhas_and_belongs_to_manyhas_one
belongs_to
serializable do
default do
attributes :id, :name
belongs_to :label
end
end[
{
"id": 503,
"label": {
"name": "Piper",
"id": 106
}
},
{
"id": 502,
"label": {
"name": "Sebrina",
"id": 77
}
}
]has_many
Works for nested relationships
class Product < ApplicationRecord
serializable do
default do
attributes :id, :name
has_many :variations
end
end
end
class Variation < ApplicationRecord
serializable do
default do
attributes :id, :name
belongs_to :color
end
end
end
class Color < ApplicationRecord
serializable do
default do
attributes :id, :hex
end
end
end[
{
"id": 503,
"variations": [
{
"name": "Cormier",
"id": 2272,
"color": {
"id": 5,
"hex": "f4b9c8"
}
},
{
"name": "Spencer",
"id": 2271,
"color": {
"id": 586,
"hex": "2e0719"
}
}
]
},
{
"id": 502,
"variations": [
{
"name": "DuBuque",
"id": 2270,
"color": {
"id": 593,
"hex": "0b288f"
}
},
{
"name": "Berge",
"id": 2269,
"color": {
"id": 536,
"hex": "b2bfee"
}
}
]
}
]has_many :through
class Product < ApplicationRecord
has_many :categories_products
has_many :categories, through: :categories_products
serializable do
default do
attributes :id
has_many :categories
end
end
end
class Category < ApplicationRecord
serializable do
default do
attributes :name, :id
end
end
end[
{
"id": 503,
"categories": [
{
"name": "Juliann",
"id": 13
},
{
"name": "Teressa",
"id": 176
},
{
"name": "Garret",
"id": 294
}
]
},
{
"id": 502,
"categories": [
{
"name": "Rossana",
"id": 254
}
]
}
]has_many_and_belongs_to_many
class Product < ApplicationRecord
has_and_belongs_to_many :categories
serializable do
default do
attributes :id
has_and_belongs_to_many :categories
end
end
end
class Category < ApplicationRecord
serializable do
default do
attributes :name, :id
end
end
end[
{
"id": 503,
"categories": [
{
"name": "Juliann",
"id": 13
},
{
"name": "Teressa",
"id": 176
},
{
"name": "Garret",
"id": 294
}
]
},
{
"id": 502,
"categories": [
{
"name": "Rossana",
"id": 254
}
]
}
]has_one
class Product < ApplicationRecord
has_one :variation
serializable do
default do
attributes :name, :id
has_one :variation
end
end
end[
{
"name": "GPS Kit",
"id": 1003,
"variation": {
"name": "Gottlieb",
"id": 4544,
"color": {
"id": 756,
"hex": "67809b"
}
}
},
{
"name": "Video Transmitter",
"id": 1002,
"variation": {
"name": "Hessel",
"id": 4535,
"color": {
"id": 111,
"hex": "144f9e"
}
}
}
]Association Traits
Models:
class Product < ApplicationRecord
has_many :variations
serializable do
default do
attributes :id, :name
end
trait :with_variations do
attributes :id
has_many :variations, trait: :for_products
end
end
end
class Variation < ApplicationRecord
serializable do
default do
attributes :id
belongs_to :color
end
trait :for_products do
attributes :id
end
end
endController:
render json: Product.limit(3).json(trait: :with_variations)Response:
[
{
"id":1,
"variations":[
]
},
{
"id":2,
"variations":[
{
"id":5
},
{
"id":4
},
{
"id":3
},
{
"id":2
},
{
"id":1
}
]
},
{
"id":3,
"variations":[
{
"id":14
},
{
"id":13
},
{
"id":12
},
{
"id":11
},
{
"id":10
},
{
"id":9
},
{
"id":8
},
{
"id":7
},
{
"id":6
}
]
}
]Single Table Inheritance
It works with single table inheritance. Traits must be defined on each class individually.
Without Rails
Rails isn't a dependency of this gem. However, setting it up to work without Rails takes some work. The specs/support folder has the initialization code needed to run the gem with just ActiveSupport and ActiveRecord.
License
The gem is available as open source under the terms of the MIT License.
Acknowledgements
Full credit Colin Rhodes for the idea.