RoR, need to create recursive relationships in table associations - mysql

I'm very new to rails, and am a little stuck on the logic for this problem.
I have one table (using mysql) of employees, each of them with a manager_id key which refers to the employee they report to. So for example the employee with the title of "CEO" with an id of 1, has a manager_id of nil, and the employee with title of "CTO" has a manager_id of 1. So my records look like this
id: 1, first_name: "Bob", last_name: "Boss", title: "CEO", manager_id: null
id: 2, first_name: "Pat", last_name: "Guy", title: "CTO", manager_id: 1
id: 3, first_name: "John", last_name: "Dude", title: "VP of engineering", manager_id: 2
and my JSON structure should look like this
[
{id: 1, first_name: "Bob", last_name: "Boss", title: "CEO", manager_id: null, descendents: [
{id: 2, first_name: "Pat", last_name: "Guy", title: "CTO", manager_id: 1, descendents: [
{id: 3, first_name: "John", last_name: "Dude", title: "VP of engineering", manager_id: 2, descendents: [....]}
]},
{..more CEO descendents...}
]
I'm trying to create a nested JSON structure that starts at CEO, lists all employees that report to them, and each of those employees descendants. I was trying to write a script that creates this but I keep getting infinite recursive calls. This is what I have
#start at some root
#root = Employee.find_by title: 'CEO'
#convert to hash table
#results[0] = #root.attributes
#add direct_reports key
#results[0]["direct_reports"] = []
def getBelow(root=#root)
#reports = Employee.where("manager_id = ?", #root[:id])
if #reports.blank?
return []
else
#reports = #reports.map(&:attributes)
#reports.each do |person|
person["direct_reports"] = []
getBelow(person)
end
#reports = Employee.where("manager_id = ?", #root[:id])
root["direct_reports"] = #reports
end
return #root
end
#list = getBelow(#results[0])
If I'm passing in each new person object, shouldn't they all eventually end when #reports.blank? becomes true?
An alternative I was thinking of was to use table associations inspired by this blog post
https://hashrocket.com/blog/posts/recursive-sql-in-activerecord
but that seems a little too complicated.

Some issues in the getBelow method
You are always using #root, instead of using the param (root). So you are always starting again from the 'CEO'.
You are calling getBelow recursively but you are not using the result.
You call #reports = Employee.where("manager_id = ?", #root[:id]) twice.
You return #root.
As Jorge Najera said, there are gems that handle a tree structure easily. If you want to build it on your own, this is my suggestion:
#start at some root
#root = Employee.find_by manager_id: nil
#convert to hash table
#tree = #root.attributes
#add direct_reports key
#tree["direct_reports"] = getBelow(#root)
def getBelow(manager)
branch = []
Employee.where("manager_id = ?", manager.id).each do |employee|
node = employee.attributes
node["direct_reports"] = getBelow(employee)
branch << node
end
branch
end
This was not tested so I think you´ll get some errors, but I believe the idea is fine.

Related

Mule condense data based on a category

Example below. I've got a set of account numbers, with an account attribute. For each account_number there are three categories, and I would like the sum for each account number based on each balance in DataWeave.
Data input
[
{
Account_Number: 1,
Account: 5,
Category: "A",
Balance: 500
},
{
Account_Number: 1,
Account: 5,
Category: "A",
Balance: 700
},
{
Account_Number: 1,
Account: 5,
Category: "B",
Balance: 300
},
{
Account_Number: 1,
Account: 5,
Category: "C",
Balance: 100
},
{
Account_Number: 2,
Account: 10,
Category: "B",
Balance: 300
},
{
Account_Number: 2,
Account: 10,
Category: "B",
Balance: 800
}
]
Data Output
[
{
Account_Number: 1,
Account: 5,
CategoryA_Balance: 1200,
CategoryB_Balance: 300,
CategoryC_Balance: 100
}
{
Account_Number: 2,
Account: 10,
CategoryA_Balance: 0,
CategoryB_Balance: 1100,
CategoryC_Balance: 0
}
]```
I assume Categories are dynamic. If not, you can replace the Categories variable with a static array.
%dw 2.0
output application/json
var byAcctNbr = payload groupBy ($.Account_Number)
var categories = payload..Category distinctBy $
---
keysOf(byAcctNbr) map ((acctNbr) ->
do {
var item = byAcctNbr[acctNbr]
var outItem = (item[0] default {}) - "Balance" - "Category"
var balances = categories reduce ((category, acc={}) ->
do {
var accounts = item filter ($.Category == category)
---
acc ++ (
("Category" ++ category ++ "_Balance"): if (isEmpty(accounts)) 0
else sum (accounts.Balance)
)
})
---
outItem ++ balances
}
)
A Similar solution to sudhish. Breaking down the solution for better understanding
distinctBy Since .. will give you all the categories present in the input. DistinctBy will remove duplicates and you will have [A,B,C]
groupBy to group based over details of each account number
(item[0] - "Balance" - "Category") Since we require AccountNumber and Account only once so used item[0] and "-" to eliminate Balance and Category since we need to perform some conditional based logic further
pluck to convert the object with account number as key to array
map iterates over the details of each account number
map over the categories will yield you [A,B,C] for both the account numbers
filter to check if the Category present in the top level map matches the categories present in the variable. if (true) then sum(Balance) else 0
sum to add based on the categories matched using filter
%dw 2.0
output application/json
var categories = payload..Category distinctBy $
---
payload groupBy $.Account_Number pluck $ map(item,index)->{
(item[0] - "Balance" - "Category"),
(categories map (cat)->{
("Category" ++ cat ++ "_Balance"):
if (isEmpty(item filter ($.Category == cat)))
0
else
sum((item filter ($.Category == cat)).Balance)
})
}

Querying to parent and children to a JSON format from MySQL 5.6?

I have a heirarchy of tables in a MySQL 5.6 database that I need to query to a JSON format for use by a javascript tree structure.
Just as a test in my flask I did the following for just the top level
def get_all_customers():
response_object = {'status': 'success'}
cnx = mysql.connector.connect(user="", password="", database="", host="localhost", port=3306)
cursor = cnx.cursor()
cursor.execute('SELECT idx, name FROM listcustomers ORDER BY name')
data = []
for idx, name in cursor:
data.append({'id': idx, 'label':name, 'otherProp': "Customer"})
response_object['customers'] = data
return jsonify(response_object)
which returns
[
{ id: 1,
label: "customer 1",
otherProp: "Customer"
},
...
]
But each customer has locations, and each location has areas, and each area has assets, and each asset has projects, and I need to also query them into children of this json object. So, for example, just going one level deeper to locations, I would need something like this -
[
{ id: 1,
label: "customer 1",
otherProp: "Customer",
children: [
{
id: 5,
label: "location 5",
otherProp: "Location"
},
...
]
},
...
]
where in my database listlocatiosn who links to listcustomers via the it's parentCustomerId column. How can I manage this? Eventually this tree will have about 13,000 objects so I know just querying the data and then parsing it with python would be far more inefficient than if I am able to query properly to begin with.

Building a JSON map for a self-referencing Ecto model

I have an Ecto model as such:
defmodule Project.Category do
use Project.Web, :model
schema "categories" do
field :name, :string
field :list_order, :integer
field :parent_id, :integer
belongs_to :menu, Project.Menu
has_many :subcategories, Project.Category, foreign_key: :parent_id
timestamps
end
#required_fields ~w(name list_order)
#optional_fields ~w(menu_id parent_id)
def changeset(model, params \\ :empty) do
model
|> cast(params, #required_fields, #optional_fields)
end
end
As you can see the Category model can reference itself via the subcategories atom.
Here is the view associated with this model:
defmodule Project.CategoryView do
use Project.Web, :view
def render("show.json", %{category: category}) do
json = %{
id: category.id,
name: category.name,
list_order: category.list_order
parent_id: category.parent_id
}
if is_list(category.subcategories) do
children = render_many(category.subcategories, Project.CategoryView, "show.json")
Map.put(json, :subcategories, children)
else
json
end
end
end
I have an if condition on subcategories so that I can play nice with Poison when they are not preloaded.
Finally, here are my 2 controller functions that invoke this view:
defmodule Project.CategoryController do
use Project.Web, :controller
alias Project.Category
def show(conn, %{"id" => id}) do
category = Repo.get!(Category, id)
render conn, "show.json", category: category
end
def showWithChildren(conn, %{"id" => id}) do
category = Repo.get!(Category, id)
|> Repo.preload [:subcategories, subcategories: :subcategories]
render conn, "show.json", category: category
end
end
The show function works fine:
{
"parent_id": null,
"name": "a",
"list_order": 4,
"id": 7
}
However, my showWithChildren function is limited to 2 levels of nesting because of how I use preloading:
{
"subcategories": [
{
"subcategories": [
{
"parent_id": 10,
"name": "d",
"list_order": 4,
"id": 11
}
],
"parent_id": 7,
"name": "c",
"list_order": 4,
"id": 10
},
{
"subcategories": [],
"parent_id": 7,
"name": "b",
"list_order": 9,
"id": 13
}
],
"parent_id": null,
"name": "a",
"list_order": 4,
"id": 7
}
For example, the category item 11 above also has subcategories but I am unable to reach them. Those subcategories can also have subcategories themselves, so the potential depth of the hierarchy is n.
I am aware that I need some recursive magic but since I'm new to both functional programming and Elixir, I cannot wrap my head around it. Any help is greatly appreciated.
You can consider doing the preloading in the view, so it works recursively:
def render("show.json", %{category: category}) do
%{id: category.id,
name: category.name,
list_order: category.list_order
parent_id: category.parent_id}
|> add_subcategories(category)
end
defp add_subcategories(json, %{subcategories: subcategories}) when is_list(subcategories) do
children =
subcategories
|> Repo.preload(:subcategories)
|> render_many(Project.CategoryView, "show.json")
Map.put(json, :subcategories, children)
end
defp add_subcategories(json, _category) do
json
end
Keep in mind this is not ideal for two reasons:
Ideally you don't want to do queries in views (but this is is recursive, so it is easier to piggyback in the view rendering)
You are going to emit multiple queries for the second level of subcategories
There is a book called SQL Antipatterns and, if I am not mistaken, it covers how to write tree structures. Your example is exposed as an antipattern in one of the free chapters. It is an excellent book and they explore solutions for all antipatterns.
PS: you want show_with_children and not showWithChildren.

Postgres JSON data type Rails query

I am using Postgres' json data type but want to do a query/ordering with data that is nested within the json.
I want to order or query with .where on the json data type. For example, I want to query for users that have a follower count > 500 or I want to order by follower or following count.
Thanks!
Example:
model User
data: {
"photos"=>[
{"type"=>"facebook", "type_id"=>"facebook", "type_name"=>"Facebook", "url"=>"facebook.com"}
],
"social_profiles"=>[
{"type"=>"vimeo", "type_id"=>"vimeo", "type_name"=>"Vimeo", "url"=>"http://vimeo.com/", "username"=>"v", "id"=>"1"},
{"bio"=>"I am not a person, but a series of plants", "followers"=>1500, "following"=>240, "type"=>"twitter", "type_id"=>"twitter", "type_name"=>"Twitter", "url"=>"http://www.twitter.com/", "username"=>"123", "id"=>"123"}
]
}
For any who stumbles upon this. I have come up with a list of queries using ActiveRecord and Postgres' JSON data type. Feel free to edit this to make it more clear.
Documentation to the JSON operators used below: https://www.postgresql.org/docs/current/functions-json.html.
# Sort based on the Hstore data:
Post.order("data->'hello' DESC")
=> #<ActiveRecord::Relation [
#<Post id: 4, data: {"hi"=>"23", "hello"=>"22"}>,
#<Post id: 3, data: {"hi"=>"13", "hello"=>"21"}>,
#<Post id: 2, data: {"hi"=>"3", "hello"=>"2"}>,
#<Post id: 1, data: {"hi"=>"2", "hello"=>"1"}>]>
# Where inside a JSON object:
Record.where("data ->> 'likelihood' = '0.89'")
# Example json object:
r.column_data
=> {"data1"=>[1, 2, 3],
"data2"=>"data2-3",
"array"=>[{"hello"=>1}, {"hi"=>2}],
"nest"=>{"nest1"=>"yes"}}
# Nested search:
Record.where("column_data -> 'nest' ->> 'nest1' = 'yes' ")
# Search within array:
Record.where("column_data #>> '{data1,1}' = '2' ")
# Search within a value that's an array:
Record.where("column_data #> '{array,0}' ->> 'hello' = '1' ")
# this only find for one element of the array.
# All elements:
Record.where("column_data ->> 'array' LIKE '%hello%' ") # bad
Record.where("column_data ->> 'array' LIKE ?", "%hello%") # good
According to this http://edgeguides.rubyonrails.org/active_record_postgresql.html#json
there's a difference in using -> and ->>:
# db/migrate/20131220144913_create_events.rb
create_table :events do |t|
t.json 'payload'
end
# app/models/event.rb
class Event < ActiveRecord::Base
end
# Usage
Event.create(payload: { kind: "user_renamed", change: ["jack", "john"]})
event = Event.first
event.payload # => {"kind"=>"user_renamed", "change"=>["jack", "john"]}
## Query based on JSON document
# The -> operator returns the original JSON type (which might be an object), whereas ->> returns text
Event.where("payload->>'kind' = ?", "user_renamed")
So you should try Record.where("data ->> 'status' = 200 ") or the operator that suits your query (http://www.postgresql.org/docs/current/static/functions-json.html).
Your question doesn't seem to correspond to the data you've shown, but if your table is named users and data is a field in that table with JSON like {count:123}, then the query
SELECT * WHERE data->'count' > 500 FROM users
will work. Take a look at your database schema to make sure you understand the layout and check that the query works before complicating it with Rails conventions.
JSON filtering in Rails
Event.create( payload: [{ "name": 'Jack', "age": 12 },
{ "name": 'John', "age": 13 },
{ "name": 'Dohn', "age": 24 }]
Event.where('payload #> ?', '[{"age": 12}]')
#You can also filter by name key
Event.where('payload #> ?', '[{"name": "John"}]')
#You can also filter by {"name":"Jack", "age":12}
Event.where('payload #> ?', {"name":"Jack", "age":12}.to_json)
You can find more about this here

Faker "Don't Know How to Build Task?

I checked out the questions that have already been asked on this subject, "There are many", but I do not find a solution.
I have a fairly large task and the files name is 'sample_employee_data.rake'... so here goes:
namespace :db do
desc "Fill database with sample Employee data"
task populate: :environment do
#gender = ["Male", "Female"]
#role = ["Staff", "Support", "Team_Leader", "Manager", "Director"]
#marital_status = ["Single", "Married"]
#primary_position = ["Household", "Job Developer", "Job Coach",
"Job Support", "Personal Care"]
#trained_position = ["Household", "Job Developer", "Job Coach",
"Job Support", "Personal Care"]
#emer_contact_relationship = ["Friend", "Brother", "Sister", "Aunt",
"Uncle", "Cousin", "Nephew", "Father",
"Mother", "Spouse"]
def randomDate(params={})
years_back = params[:year_range] || 5
latest_year = params[:year_latest] || 0
year = (rand * (years_back)).ceil +
(Time.now.year - latest_year - years_back)
month = (rand * 12).ceil
day = (rand * 31).ceil
series = [date = Time.local(year, month, day)]
if params[:series]
params[:series].each do |some_time_after|
series << series.last + (rand * some_time_after).ceil
end
return series
end
date
end
Employee.create!(first_name: "Shelly",
last_name: "Houghton",
mi: "M",
full_name: "Shelly M Houghton",
marital_status: "Single",
gender: "Female",
hire_date: "2000-04-16",
primary_position: "Manager",
trained_position: "Job Developer",
email: "shoughton#example.com",
active: true,
address1: "76th Ave",
city: "Frave",
zip_code: "54806",
state: "WI",
emp_home_ph: "1-111-111-1111",
emp_mobile_ph: "1-222-222-2222",
emer_contact_first_name: "Kenneth",
emer_contact_last_name: "Koening",
emer_contact_relationship: "Friend",
emer_contact_ph: "1-333-333-3333",
role: "Manager",
birth_date: "1982-08-21",
admin: true,
password: "90nothguoh",
password_confirmation: "90nothguoh")
99.times do |n|
first_name = Faker::Name.first_name
last_name = Faker::Name.last_name
mi = ("A".."Z").to_a[rand(26)]
full_name = Faker::Name.full_name
marital_status = #marital_status[rand(2)].to_s
gender = #gender[rand(2)].to_s
hire_date = randomDate(:year_range => 60, :year_latest => 12)
birth_date = randomDate(:year_range => 60, :year_latest => 22)
primary_position = #primary_position[rand(5)].to_s
trained_position = #trained_position[rand(5)].to_s
email = "emp-#{n+1}#example.org"
active = [true, false][rand(2)]
admin = (1 == rand(2) ? true : false)
role = #role[rand(5)].to_s
address1 = "Seaview-#{n+5}Way"
city = Faker::Lorem.words(1).to_s.capitalize
state = Faker::Address.us_state()
zip_code = Faker::Address.zip_code
emp_home_ph = Faker::PhoneNumber.phone_number
emp_mobile_ph = Faker::PhoneNumber.phone_number
emer_contact_first_name = Faker::Name.first_name
emer_contact_last_name = Faker::Name.last_name
emer_contact_relationship = #emer_contact_relationship[rand(10)].to_s
emer_contact_ph = Faker::PhoneNumber.phone_number
password = "uniqueone"
Employee.create!(first_name: first_name, mi: mi, last_name: last_name,
full_name: full_name, marital_status: marital_status,
gender: gender, birth_date: birth_date, hire_date: hire_date,
primary_position: primary_position, trained_position:
trained_position, email: email, role: role, address1:
address1, city: city, state: state, zip_code: zip_code,
emp_home_ph: emp_home_ph, emp_mobile_ph: emp_mobile_ph,
emer_contact_first_name: emer_contact_first_name,
emer_contact_last_name: emer_contact_last_name,
emer_contact_relationship: emer_contact_relationship,
emer_contact_ph: emer_contact_ph, password: password,
password_confirmation: password)
end
end
end
I ran:
rake sample_employee_data.rake
and I got this flag:
rake aborted!
Don't know how to build task 'sample_employee_data.rake'
home/me/.rvm/gems/ruby-1.9.3-p385#rails3212/bin/ruby_noexec_wrapper:14:in 'eval'
home/me/.rvm/gems/ruby-1.9.3-p385#rails3212/bin/ruby_noexec_wrapper:14:in '<main>'
Can anyone spot the problem ... my apologies for such a long file.
Thanks.
There's a couple things going on here.
When you run $ rake sample_employee_data.rake you're asking rake to rake your file - but rake responds to tasks:
$ rake some_namespace:some_task_in_that_namespace
You're also using a custom .rake file rather than the conventional Rakefile - which is fine, but this means you need to tell rake explicitly that you're not using its default Rakefile.
$ rake --rakefile your_custom_rake_file.rake some_task
So in your case, you need to issue a rake command like so:
$ rake --rakefile sample_employee_data.rake db:populate