How to get tree data for self join model - ruby-on-rails

I have model which place have many child places as bellow
class Place < ActiveRecord::Base
belongs_to :parent , class_name: "Place", foreign_key: "parent_id"
has_many :childs , class_name: "Place", foreign_key: "parent_id"
end
and i want to get data from this model in bellow form to represent in tree
data = [
{
label: 'place',
children: [
{ label: 'child1' , childern: [ {label: 'child11'} , {label: 'child12'}] },
{ label: 'child2' , childern: [ {label: 'child21'} , {label: 'child22'}] }
]
},
{
label: 'place',
children: [
{ label: 'child3' }
]
}
]
I started with this function
def get_tree(Place)
data = []
Place.all.each do |place|
dataInner= {label: place.name ,id: place.id}
children = [] # to hold childern data
place.childs.each do |child|
childhash = {label: child.name , id: child.id }
children.push(childhash)
end
dataInner.merge!(children: children) # push childern
data.push(dataInner)
end
return data
end
This function work ok but get depth 1 of childs only.
I want to get tree with any depth of childs

try something like
def get_tree(node)
return {label: node.name} if node.childs.empty?
{label: node.name, children: node.childs.collect { |v| get_tree(v) }
end

Related

ROR Self Join for self reference

I am using the Self Joins to add reference to its own table.
class Employee < ApplicationRecord
belongs_to :manager, class_name: "Employee", optional: true
has_many :customers
end
class Customer
belongs_to :employee
end
I am fetching the employee data based. One employee will have one employee_head and employee_head will have a boss.
In the serializer i have
class EmployeeSerializer < ApplicationSerializer
# include FastJsonapi::ObjectSerializer
attributes :id, :name, :manager
def manager
ActiveModel::SerializableResource.new(object.manager)
end
end
when i query form employee, i am expecting:
{
employee:
{
id: 1,
name: "employee name",
customer_attributes: [
{ id: 8, name: customer_name }
],
manager: {
id: 2,
name: "employee head name",
customer_attributes: [
{ id: 8, name: customer_name }
],
manager: {
id: 3,
name: "boss name",
customer_attributes: [
{ id: 8, name: customer_name }
],
}
}
}
}
I am getting the expected data using:
Employee.includes(:customers).where(name: "employee name", employee_type: employee)
but the problem is when i hit the query, the customer information is fetched from the database multiple time. I am confuse where to use includes to avoid N+1 query to DB.
Thanks in advance

How to merge attributes from associated models into one nested attribute in serializer?

Below is my json after rendering '#products'. As you can see, there are 2 other models nested (vendor_products and vendors). The association between product and vendor models is many-to-many with 'vendor_products' being connecting tables. What I wanted to achieve here is - instead of having both 'vendor_products' and 'vendors' models being nested I just want to add 'vendor name' as another attribute inside the vendor_products model.
{
id: 1,
barcode: 3045320001525,
name: "xyz",
size: "370 g",
brand: "abc",
img_url: "http://xyx"
vendor_products: [
{
id: 1,
v_item: "JAM101",
vendor_id: 1,
case_price: 72
},
{
id: 2,
v_item: "1001",
vendor_id: 2,
case_price: 65
}
],
vendors: [
{
name: "vendor_xyz"
},
{
name: "vendor_123"
}
]
},
Below is the format of json I wanted:
{
id: 1,
barcode: 3045320001525,
name: "xyz",
size: "370 g",
brand: "abc",
img_url: "http://xyx"
vendor_products: [
{
id: 1,
v_item: "JAM101",
vendor_id: 1,
vendor_name: "vendor_xyz",
case_price: 72
},
{
id: 2,
v_item: "1001",
vendor_id: 2,
vendor_name: "vendor_abc",
case_price: 65
}
],
Here are my serializer classes:
class ProductSerializer < ActiveModel::Serializer
attributes :id, :barcode, :name, :size, :brand, :img_url
has_many :vendor_products
has_many :vendors
end
class VendorProductSerializer < ActiveModel::Serializer
attributes :id, :v_item, :vendor_id, :case_price
belongs_to :product
belongs_to :vendor
end
class VendorSerializer < ActiveModel::Serializer
attributes :name
has_many :products
has_many :vendor_products
end
Try adding a custom attribute in vendor_products serializer,
class VendorProductSerializer < ActiveModel::Serializer
attributes :id, :v_item, :vendor_id, :case_price, :vendor_name
belongs_to :product
belongs_to :vendor
def vendor_name
object.vendor.name #object is current vendor_product object get name from that
end
end

How to include child associations when serializing to json?

Before using fast_jsonapi gem I was doing this:
render json: school.to_json(include: [classroom: [:students]])
My SchoolSerializer looks like:
class SchoolSerializer
include FastJsonapi::ObjectSerializer
attributes :name, :description, :classroom
end
How would I get the students included in the JSON result?
Also, the classroom association is including but it is displaying all the properties, is there a way to map the classroom property to a ClassroomSerializer ?
class School < ApplicationRecord
belongs_to :classroom
end
class Classroom < ApplicationRecord
has_many :students
end
class SchoolSerializer
include FastJsonapi::ObjectSerializer
attributes :name, :description
belongs_to :classroom
end
# /serializers/classroom_serializer.rb
class ClassroomSerializer
include FastJsonapi::ObjectSerializer
attributes :.... #attributes you want to show
end
Also you can add additional association to your School model, to access Students.
like this
has_many :students, through: :classroom
and then include it in School serializer directly.
Update: also please note that you can directly point to serializer class you need. (if you want to use class with different name from model as example).
class SchoolSerializer
include FastJsonapi::ObjectSerializer
attributes :name, :description
belongs_to :classroom, serializer: ClassroomSerializer
end
render json: SchoolSerializer.new(school, include: "classrooms.students")
The difference being the use of "include" when rendering the serializer. This tells the Serializer to add a key "included" to the returned JSON object.
class SchoolSerializer
include FastJsonapi::ObjectSerializer
belongs_to :classroom
has_many :students, through: :classroom
attributes :school_name, :description
end
StudentSerializer
include FastJsonapi::ObjectSerializer
belongs_to :classroom
belongs_to :school
attributes :student_name
end
render json: SchoolSerializer.new(school).serialized_json
will return a series of students with only the top level identifiers in the form
data: {
id: "123"
type: "school"
attributes: {
school_name: "Best school for Girls",
description: "Great school!"
...
},
relationships: {
students: [
{
id: "1234",
type: "student"
},
{
id: "5678",
type: "student"
}
]
}
}
whereas the include: "classroom.students" will return the full serialized Student Records in the form:
data: {
id: "123"
type: "school"
attributes: {
school_name: "Best school for Girls"
...
},
relationships: {
classroom: {
data: {
id: "456",
type: "classroom"
}
},
students: [
{
data: {
id: "1234",
type: "student"
}
},
{
data: {
id: "5678",
type: "student"
}
}
]
},
included: {
students: {
data {
id: "1234",
type: "student",
attributes: {
student_name: "Ralph Wiggum",
...
},
relationships: {
school: {
id: "123",
type: "school"
},
classroom: {
id: "456",
type: "classroom"
}
}
},
data: {
id: "5678",
type: "student",
attributes: {
student_name: "Lisa Simpson",
...
},
relationships: {
school: {
id: "123",
type: "school"
},
classroom: {
id: "456",
type: "classroom"
}
}
}
},
classroom: {
// Effectively
// ClassroomSerializer.new(school.classroom).serialized_json
},
}
}

How to specify a different root name for the embedded objects?

In my app I had BlogPost model and User model that are related through relation named author. To serve data from my Rails app I use active_model_serializers with definition:
class Blog::PostSerializer < ActiveModel::Serializer
embed :ids, include: true
attributes :id, :title, :text, :created_at, :updated_at
has_one :author
has_many :assets
end
When I fetch this using Ember model:
Admin.BlogPost = DS.Model.extend({
author: DS.belongsTo('User'),
title: DS.attr('string'),
text: DS.attr('string'),
createdAt: DS.attr('date'),
updatedAt: DS.attr('date')
});
There is an error:
Uncaught Error: Assertion Failed: You looked up the 'author' relationship on a 'blog.post' with id 1 but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async (`DS.belongsTo({ async: true })`)
Which is caused by that my response looks like:
{
'blog_posts': [
{
id: 1,
author_id: 1
},
// …
],
'authors': [
{ id: 1, /* … */ }
]
}
Is there any way to change 'authors' in response to 'users' or use 'authors' as alias to 'users' in serializer?
From active_model_serializers 0.8 description: https://github.com/rails-api/active_model_serializers/tree/0-8-stable
You can also specify a different root for the embedded objects than the key used to reference them:
class PostSerializer < ActiveModel::Serializer
embed :ids, :include => true
attributes :id, :title, :body
has_many :comments, :key => :comment_ids, :root => :comment_objects
end
This would generate JSON that would look like this:
{"post": {
"id": 1,
"title": "New post",
"body": "A body!",
"comment_ids": [ 1 ]
},
"comment_objects": [
{ "id": 1, "body": "what a dumb post" }
]
}
Just define a method in your serializer named users and return authors in it I.e.
attributes :id, :title, :text, :created_at, :updated_at, :users
def users
object.authors
end

tree structure in ruby with parent child in array format without gems?

I have a array which have list of item like this
arr = [
{:id=>1, :title=>"A", :parent_id=>nil},
{:id=>2, :title=>"B", :parent_id=>nil},
{:id=>3, :title=>"A1", :parent_id=>1},
{:id=>4, :title=>"A2", :parent_id=>1},
{:id=>5, :title=>"A11", :parent_id=>3},
{:id=>6, :title=>"12", :parent_id=>3},
{:id=>7, :title=>"A2=121", :parent_id=>6},
{:id=>8, :title=>"A21", :parent_id=>4},
{:id=>9, :title=>"B11", :parent_id=>2},
{:id=>10, :title=>"B12", :parent_id=>2},
...
]
If parent_id is nil then its should be the parent node, if parent_id is not nil then it should comes under the particular parent.
Based on id and parent_id, I want to provide a response like this:
-A
-A1
-A11
-A12
-A123
-A2
-A21
-B
-B1
-B11
-B12
How could I generate a responds mentioned above?
This is easier than you think, you just need to realize a couple simple things:
nil is a perfectly valid Hash key.
You can use nil as a virtual root for your tree so that all the :parent_ids point at things in your tree.
You can iterate through the array and track entries in two ways at once: by :id and by :parent_id.
First a tree represented by a Hash:
tree = Hash.new { |h,k| h[k] = { :title => nil, :children => [ ] } }
We're going to be going from the root to the leaves so we're only interested in the children side of the parent/child relationship, hence the :children array in the default values.
Then a simple iteration that fills in the :titles and :children as it goes:
arr.each do |n|
id, parent_id = n.values_at(:id, :parent_id)
tree[id][:title] = n[:title]
tree[parent_id][:children].push(tree[id])
end
Note that the nodes (including the parent nodes) are automatically created by tree's default_proc the first time they're seen so the node order in arr is irrelevant.
That leaves us with the tree in tree where the keys are :ids (including the virtual root at the nil key) and the values are subtrees from that point.
Then if you look at tree[nil][:children] to peel off the virtual root, you'll see this:
[
{ :title => "A", :children => [
{ :title => "A1", :children => [
{ :title => "A11", :children => [] },
{ :title => "12", :children => [
{ :title => "A2=121", :children => [] }
] }
] },
{ :title => "A2", :children => [
{ :title => "A21", :children => [] }
] }
] },
{ :title => "B", :children => [
{ :title => "B11", :children => [] },
{ :title => "B12", :children => [] }
] }
]
and that has exactly the structure you're looking for and you should be able to take it from there. That doesn't match your sample response but that's because your sample arr doesn't either.
You could also say:
tree = arr.each_with_object(Hash.new { |h,k| h[k] = { :title => nil, :children => [ ] } }) do |n, tree|
#...
end
if you preferred that rather noisy first line to a separate tree declaration.
Some thing like this will work:
parents = arr.select{|hash| hash[:parent_id] == nil }
parents.each {|hash| print_children hash, arr, "-"}
def print_children(hash, arr, spaces)
puts spaces + hash[:title]
spaces = ' ' + spaces
children = arr.select{|all_hash| all_hash[:parent_id] == hash[:id] }
children.each { |child_hash| print_children child_hash, arr, spaces }
end

Resources