Presenting a hypothetical, simplified version of my problem. Imagine I have a website where users can create a Shop with their own branding and choose from a catalog of Products to show in their Shop. Shops & Products have a has-and-belongs-to-many (HABTM) relationship. Each Product has its own Shop-specific route.
Rails.application.routes.draw do
resources :shops do
resources :products
end
end
class ShopSerializer < ActiveModel::Serializer
has_many :products
end
class ProductSerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
attribute :url do
shop_product_url(NEED SHOP ID, product_id: object.id)
end
end
When a Shop is serialized, and as a result, so is the collection of its Products, I want the Product serializer to be aware of the shop that is serializing it and use that to include the route in the serialized output. How is this possible? I've tried all manner of passing instance_options from the ShopSerializer but it doesn't work as expected.
# this works except is apparently not threadsafe as multiple
# concurrent requests lead to the wrong shop_id being used
# in some of the serialized data
has_many :products do
ActiveModelSerializers::SerializableResource.new(shop_id: object.id).serializable_hash
end
# shop_id isn't actually available in instance_options
has_many :products do
ProductSerializer.new(shop_id: object.id)
end
Unfortunately serializer associations do not seem to provide a clean way to pass in custom attributes to the child serializers. There are a few not-so-pretty solutions though.
1. Invoke ProductSerializer manually, add URL in ShopSerializer
class ProductSerializer < ActiveModel::Serializer
end
class ShopSerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
attribute :products do
object.products.map do |product|
ProductSerializer.new(product).serializable_hash.merge(
url: shop_product_url(object.id, product_id: product.id)
)
end
end
end
2. Add shop ID to the Product instances before they are fed to ProductSerializer
class ProductSerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
attribute :url do
shop_product_url(object.shop_id, product_id: object.id)
end
end
class ShopSerializer < ActiveModel::Serializer
has_many :products, serializer: ProductSerializer do
shop = object
shop.products.map do |product|
product.dup.tap do |instance|
instance.singleton_class.send :define_method, :shop_id do
shop.id
end
end
end
end
end
Both solutions should be thread safe, but the first solution seems like a better idea to me, as the second one makes ProductSerializer unusable on its own — i.e. when just a single Product is serialized without knowing the particular shop it should belong to.
Related
I have three serializers, nested within each other. Like this:
class PersonSerializer < ActiveModel::Serializer
attributes :id :name
has_many: companies
class Company < ActiveModel::Serializer
has_many :products
class ProductSerializer < ActiveModel::Serializer
has_many :product_items do
unless person.id != object.company.user_id
object.product_items
end
end
end
end
end
My issue is the line: unless person.id != object.company.user_id.
person is undefined here. How do I get access to the current person instance within the ProductSerializer?
I am unsure why would you need that structure, but general class definition declares a new scope. Whether you need to capture variables from the parent scope, use closure with Class#new instead:
class PersonSerializer < ActiveModel::Serializer
attributes :id :name
has_many: companies
Company = Class.new(ActiveModel::Serializer) do
has_many :products
ProductSerializer = Class.new(ActiveModel::Serializer) do
has_many :product_items do
unless person.id != object.company.user_id
object.product_items
end
end
end
end
end
I think it is more of a "Model Design" issue than a rails issue.
For clarity sake here is the business logic: I've Venues and I want to implement multiple APIs to get data about those venues. All this APIs have a lot in common, therefore I used STI.
# /app/models/venue.rb
class Venue < ApplicationRecord
has_one :google_api
has_one :other_api
has_many :apis
end
# /app/models/api.rb
class Api < ApplicationRecord
belongs_to :venue
end
# /app/models/google_api.rb
class GoogleApi < Api
def find_venue_reference
# ...
end
def synch_data
# ...
end
end
# /app/models/other_api.rb
class OtherApi < Api
def find_venue_reference
# ...
end
def synch_data
# ...
end
end
That part works, now what I'm trying to add is Photos to the venue. I will be fetching those photos from the API and I realise that every API might be different. I thought about using STI for that as well and I will end up with something like that
# /app/models/api_photo.rb
class ApiPhoto < ApplicationRecord
belongs_to :api
end
# /app/models/google_api_photo.rb
class GoogleApiPhoto < ApiPhoto
def url
"www.google.com/#{reference}"
end
end
# /app/models/other_api_photo.rb
class OtherApiPhoto < ApiPhoto
def url
self[url] || nil
end
end
My goal being to have this at the end
# /app/models/venue.rb
class Venue < ApplicationRecord
has_one :google_api
has_one :other_api
has_many :apis
has_many :photos :through => :apis
end
# /app/views/venues/show.html.erb
<%# ... %>
#venue.photos.each do |photo|
photo.url
end
<%# ... %>
And photo.url will give me the right formatting that is dependent of the api it is.
As I'm going deeper in the integration, something seems not right. If I had to Api the has_many :google_api_photo then every Api will have GoogleApiPhoto. What does not make sense to me.
Any idea how I should proceed from here?
I think I solved it.
By adding this to venue.rb
has_many :apis, :dependent => :destroy
has_many :photos, :through => :apis, :source => :api_photos
By calling venue.photos[0].url call the right Class based on the type field of the ApiPhoto
I have an hierarchical structure in my app:
Environment has Containers
Container has Items
Item has expression
so my Model code looks like:
class Environment < ActiveRecord::Base
has_many :containers, :dependent => :destroy
def as_json(options = {})
super(options.merge(include: :containers))
end
end
class Container < ActiveRecord::Base
has_many :items, :dependent => :destroy
belongs_to :environment
def as_json(options = {})
super(options.merge(include: :items))
end
end
class Item < ActiveRecord::Base
has_many :expressions, :dependent => :destroy
belongs_to :container
def as_json(options = {})
super(options.merge(include: :expressions))
end
end
class Expression < ActiveRecord::Base
belongs_to :item
def as_json(options = {})
super()
end
end
In a regular get of a record I usually need only one hierarchy below the desired record, that's why in the as_json I merge only one hierarchy down (get Environment will return a collection of containers but those containers will not have Items)
My Question:
Now what I need is to add a method to the controller that allows full hierarchy response i.e. GET /environment/getFullHierarchy/3 will return: environment with id=3 with all its containers and for every container all it's Items & for every Item all it's expressions. without breaking the current as_json
I'm kinda new to Rails, wirking with Rails 4.2.6 & don't know where to start - can anyone help?
Sure it goes something like this hopefully you get the idea.
EnvironmentSerializer.new(environment) to get the hierarchy json.
lets say environments table has columns environment_attr1 , environment_attr2
class EnvironmentSerializer < ActiveModel::Serializer
attributes :environment_attr1, :environment_attr2 , :containers
# This method is called if you have defined a
# attribute above which is not a direct value like for
# a rectancle serializer will have attributes length and width
# but you can add a attribute area as a symbol and define a method
# area which returns object.length * object.width
def containers
ActiveModel::ArraySerializer.new(object.containers,
each_serializer: ContainerSerializer)
end
end
class ContainerSerializer < ActiveModel::Serializer
attributes :container_attr1, :container_attr2 , :items
def items
ActiveModel::ArraySerializer.new(object.items,
each_serializer: ItemSerializer)
end
end
class ItemSerializer < ActiveModel::Serializer
...
end
class ExpressionSerializer < ActiveModel::Serializer
...
end
I'm having a bit trouble with the namespaces in Rails 4.
I have ActiveRecord models Shop, Order, and OrderItem
# model/shop.rb
class Shop < ActiveRecord::Base
# model/order.rb
class Order < ActiveRecord::Base
has_many :order_items
# model/order_item.rb
class OrderItem < ActiveRecord::Base
belongs_to :orderable, polymorphic: true
belongs_to :order
I'm replicating the relationship between Order and OrderItem in a namespace like this
# model/shop/order.rb
class Shop::Order
attr_accessor :order_items
def initialize
self.order_items = []
self.order_items << Shop::OrderItem.new
end
# model/shop/order_item.rb
class Shop::OrderItem
attr_accessor :orderable_type, :orderable_id
def initialize(params = {})
if params
self.orderable_type = params['orderable_type'] if params['orderable_type']
self.orderable_id = params['orderable_id'] if params['orderable_id']
end
end
def price
orderable.price
end
def orderable
orderable_type.constantize.find_by(id: orderable_id)
end
def to_h
Hash[
orderable_type: self.orderable_type,
orderable_id: self.orderable_id,
price: self.price
]
end
end
So my problem is that when I initialize Shop::Order.new, sometimes its order_items is an array of OrderItems instead of Shop::OrderItems, and when I test it in the controller, if I type Shop::OrderItem, it will return OrderItem.
I'm wondering if Shop::OrderItem wasn't initialized before OrderItem and cause the issue?
You are running into a namespace collision. Depending on where the code is executing, Shop could be the ActiveRecord model that you've defined in models/shop.rb, or it could be the module namespace that you've defined under models/shops/*.rb. Not only will this cause unpredictable execution, it's also confusing to read.
I recommend using a module namespace other than "Shop". Even calling it "MyShop" would be an improvement. However you'll probably still run into naming collisions between Shop and MyShop::Shop. You should probably rename the Shop class under the MyShop module to avoid this:
For example:
# model/my_shop/my_order.rb
class MyShop::MyOrder
# ...
end
# model/my_shop/my_order_item.rb
class MyShop::MyOrderItem
# ...
end
Having said all that, I feel like you're setting yourself up for a world of hurt. This problem might be better solved using service objects. Google up "Rails Service Objects" for some really good examples.
I have used AMS (0.8) with Rails 3.2.19 but one place where I really struggle with them is how to control whether serializers include their associations or not. I obviously use AMS to build JSON
Api's. Sometimes a serializer is the leaf or furthest out element and sometimes it's the top level and needs to include associations. My question is what is the best way to do this or is the solution I do below work (or is best solution).
I have seen some of the discussions but I find them very confusing (and version based). It's clear that for Serializer attributes or associations, there is an an include_XXX? method for each and you can return either a truthy or falsey statement here.
Here's my proposed code - it's a winemaker that has many wine_items. Is this how you would do this?
Model Classes:
class WineItem < ActiveRecord::Base
attr_accessible :name, :winemaker_id
belongs_to :winemaker
end
class Winemaker < ActiveRecord::Base
attr_accessible :name
has_many :wine_items
attr_accessor :show_items
end
Serializers:
class WinemakerSerializer < ActiveModel::Serializer
attributes :id, :name
has_many :wine_items
def include_wine_items?
object.show_items
end
end
class WineItemSerializer < ActiveModel::Serializer
attributes :id, :name
end
and in my controller:
class ApiWinemakersController < ApplicationController
def index
#winemakers=Winemaker.all
#winemakers.each { |wm| wm.show_items=true }
render json: #winemakers, each_serializer: WinemakerSerializer, root: "data"
end
end
I ran into this issue myself and this is the cleanest solution so far (but I'm not a fan of it).
This method allows you to do things like:
/parents/1?include_children=true
or using a cleaner syntax like:
/parents/1?include=[children], etc...
# app/controllers/application_controller.rb
class ApplicationController
# Override scope for ActiveModel-Serializer (method defined below)
# See: https://github.com/rails-api/active_model_serializers/tree/0-8-stable#customizing-scope
serialization_scope(:serializer_scope)
private
# Whatever is in this method is accessible in the serializer classes.
# Pass in params for conditional includes.
def serializer_scope
OpenStruct.new(params: params, current_user: current_user)
end
end
# app/serializers/parent_serializer.rb
class ParentSerializer < ActiveModel::Serializer
has_many :children
def include_children?
params[:include_children] == true
# or if using other syntax:
# params[:includes].include?("children")
end
end
Kinda hackish to me, but it works. Hope you find it useful!