15

I am using LARAVEL 4 with MySQL back-end.

I have a self-referencing table with columns id, name, type and parent. Here, parent is foreign-key of the column Id. The data in table is as below :

id  name          type         parent 
1   General       group        NULL
2   What is..?    question     1
3   aa            answer       2
4   bb            answer       2
5   cc            answer       2
6   How is..?     question     1
7   ba            answer       6
8   bb            answer       6
9   Where is..?   question     4
10  ca            answer       9
11  cb            answer       9
12  Who is..?     question     6
13  da            answer       12
14  db            answer       12
15  Specific      group        NULL
16  When is..?    question     15
17  ea            answer       16
18  eb            answer       16
19  Whome is..?   question     2
20  fa            answer       19
21  fb            answer       19
22  fc            answer       19

I want a function that return nested JSON using this relational data. For example :

[{
  "id" : 1, 
  "name" : "Geneal", 
  "type" : "group", 
  "children" : [{
      "id" : 2, 
      "name" : "What is..?", 
      "type" : "question", 
      "children" : [{
         "id" : 3, 
         "name" : "aa", 
         "type" : "answer"
      },
      {
         "id" : 4, 
         "name" : "bb", 
         "type" : "answer"
      },
      {
         "id" : 5, 
         "name" : "cc", 
         "type" : "answer"
      }]},
      {
      "id" : 6, 
      "name" : "How is..?", 
      "type" : "question", 
      "children" : [{
         "id" : 7, 
         "name" : "ba", 
         "type" : "answer"
      },
      {
         "id" : 8, 
         "name" : "bb", 
         "type" : "answer"
      }]
   }]
... and so on
}]

I have created a model named Survey as below :

class Survey extends BaseModel{

    protected $table = 'questions';
    protected $softDelete = false;

    public function parent()
    {
        return $this->belongsTo('Survey', 'parent');
    }

    public function children()
    {
        return $this->hasMany('Survey', 'parent');
    }   
}

and called it in controller with :

$user = Survey::all();
$parent = $user->parent()->first();
$children = $user->children()->get();

But I am not getting the proper result as I have mentioned in JSON above.

print_r($parent->toJson()); 

gives records with one level hierarchy only (i.e group and questions, not answers).

while

print_r($children ->toJson());

Gives only questions (Not groups and answers).

I want the whole self-referencing data in nested JSON format with N level of hierarchy.

I also have tried

$user = Survey::with('parent', 'children')->get();

But found same as $parent above.

Is there anyway I can get the desired result?

Thanks in advance..

Dev
  • 6,570
  • 10
  • 66
  • 112
  • I think LARAVEL is use-less framework. I am not finding any help anywhere in its document itself. – Dev Jul 10 '14 at 10:41
  • I think no experts available for LARAVEL on stackoverflow now – Dev Jul 10 '14 at 12:01
  • Check this: https://github.com/etrepat/baum – Hkan Jul 10 '14 at 12:03
  • 1
    Read the docs http://laravel.com/docs/eloquent#eager-loading *nested relations*, try it, then come back if you still need help. – Jarek Tkaczyk Jul 10 '14 at 12:33
  • @deczo I already have mentioned in my question that I have used eager-loading. Please see $user = Survey::with('parent', 'children')->get(); This statement only retrieves group and questions, not answers. I want it as nested array or JSON with N level. How to achieve it? – Dev Jul 10 '14 at 13:01
  • @Dev I was talking about *nested relations*. Anyway check my answer – Jarek Tkaczyk Jul 10 '14 at 14:21
  • @deczo LARAVEL Experts are there on the stackoverflow - the amazing site. – Dev Jul 11 '14 at 12:03

2 Answers2

56

Here's how you manually retrieve nested relations:

$collection = Model::with('relation1.relation2.relation3')->get();

So in your case it would be:

$surveys = Survey::with('children.children.children')->get();

Obviously this will do the job when the relations are fixed, but it's not the way to go for a recursive relation to the same table.

Fortunately, you can make such relation recursive, then all you need to retrieve whole tree is this:

$surveys = Survey::with('childrenRecursive');

However, I wouldn't load parent for each row this way.

So here's all you need:

// Survey model
// loads only direct children - 1 level
public function children()
{
   return $this->hasMany('Survey', 'parent');
}

// recursive, loads all descendants
public function childrenRecursive()
{
   return $this->children()->with('childrenRecursive');
   // which is equivalent to:
   // return $this->hasMany('Survey', 'parent')->with('childrenRecursive);
}

// parent
public function parent()
{
   return $this->belongsTo('Survey','parent');
}

// all ascendants
public function parentRecursive()
{
   return $this->parent()->with('parentRecursive');
}

EDIT: To get real tree structure, first query must be limited to only root nodes:

$surveys = Survey::with('childrenRecursive')->whereNull('parent')->get();
Jarek Tkaczyk
  • 78,987
  • 25
  • 159
  • 157
  • Thanks for the answer. Unfortunately I am getting performance issue in this implementation. I have just 1047 records in this table still it takes too long to load this data (Almost 10 minutes, of course I have computer with good configuration.). There can be thousands of records in this table. In that case I assume it will not work. Have you any solution to increase performance? – Dev Jul 11 '14 at 05:12
  • I don't think it is giving me any array of table. There must be some error in recursion. I am getting 181415 rows for the call $surveys = Survey::with('childrenRecursive'); but none of this having any field of the table. – Dev Jul 11 '14 at 06:02
  • It's giving correct results, maybe you just exhausted your ram with 180 thousand item array.. Try with sensible limit. – Jarek Tkaczyk Jul 11 '14 at 08:16
  • OK. It might give the correct result but I cannot use it if it creates a performance issue. Is there any technique if you know to overcome this? I am novice to php especially laravel. – Dev Jul 11 '14 at 08:51
  • 1
    Loading 200 000 rows to an array will always cause memory issue. Process it in chunks, paginate or whatever. I can't suggest if I don't know what you do with that data. – Jarek Tkaczyk Jul 11 '14 at 09:37
  • I want to display it in treeview. – Dev Jul 11 '14 at 09:44
  • However, if I use $group = Survey::with('parent' ,'childrenRecursive')->get(); to get the records then it gives the desired result so fast but in that case I am finding problem of duplication with actual result. The duplicate child records are added at 0th level. Have you any solution for this? I think we are close to it. – Dev Jul 11 '14 at 11:22
  • Don't load `parent`. Duplicates are obvious, because you need to first select only root level rows. Check the edit. – Jarek Tkaczyk Jul 11 '14 at 11:51
  • Oh My GOD !!! It worked. :) Thank you so much.. It really worked.. Thanks for support. I am accepting this answer as well as +1 for your kind help. :) Thanks alot.. – Dev Jul 11 '14 at 12:00
  • What if I want a column from another table to this model. For example there is a table named "template" which is referencing to the "survey" table with column "survey.template_id". Now I want the "template.title" column from "templates" table. How can I achieve it? I have tried left join but I haven't succeed. – Dev Jul 30 '14 at 06:48
  • Is it the same http://stackoverflow.com/questions/25016893/get-column-of-other-table-in-laravel-orm-from-self-referencing-table-get-n-level/25030966#25030966 ? – Jarek Tkaczyk Jul 30 '14 at 07:25
  • Hmm.. I think so but I need to maintain the hierarchy (i.e. I have explained in this question. Will your link work the same? – Dev Jul 30 '14 at 10:14
  • I don't get what you mean by maintaining hierarchy. Anyway, check that related link, use `leftJoin`, and if it's not what you need, then ask a new question with proper description of what you want to achieve. – Jarek Tkaczyk Jul 30 '14 at 11:07
  • I have posted a new question with proper description. I request you to follow this link http://stackoverflow.com/questions/25051501/laravel-eloquent-get-n-level-hierarchy-records-from-self-referencing-table-with and provide solution if possible. – Dev Jul 31 '14 at 05:38
  • can any one solve my same issue at https://stackoverflow.com/questions/48768670/illegal-offset-type-error-on-fetching-a-relations-to-self-class-in-laravel. I tried this answer but I got `Illegal offset type` error – Ahmad Badpey Feb 14 '18 at 04:30
3

so simple!!!

1- load your 'children' relation in your 'children' relation in your model like this :

class Survey extends BaseModel{

    protected $table = 'questions';
    protected $softDelete = false;

    public function parent()
    {
        return $this->belongsTo('Survey', 'parent');
    }

    public function children()
    {
       // change this: return $this->hasMany('Survey', 'parent');

        return $this->hasMany('Survey', 'parent')->with('children);
    }   
}

2- you just need add clouser function to your relation like this:

$surveys = Survey::with(['children' => function($q) {
            $q->with('children');
       },
       'parent' => function($q) {
            $q->with('parent');
       });

then you can access to your nested children in your result like this:

$surveys->children

and unlimited nesting :

$surveys->children->first()->children

and so on.