1

I have a Location model that can have many sublocations, or one parent location. Let's say location-A is a parent location, has location-B and location-C as sublocations. But location-B also has a sublocation location-D.

How can I get all sublocations of the parent location-A, including location-D?

My model:

  has_many :sub_locations, class_name: "Location", foreign_key: "parent_location_id", inverse_of: :parent_location
  belongs_to :parent_location, optional: true, class_name: "Location", foreign_key: "parent_location_id", inverse_of: :sub_locations

Currently I fetch them like this:

  def all_sub_location_ids
    [id] + sub_locations.map(&:all_sub_location_ids).flatten
  end

But I need an efficient way. Because it throws stack level too deep

Edit: I ended up using .reload on sublocations and keep the existing method as is. This way it worked.

cnn
  • 13
  • 3
  • 1
    Iteration is definitely not your best bet. While you could implement a CTE to handle this case there are also a few gems that have already worked out many of the details with this and offer other useful features as well, with [`ancestry`](https://github.com/stefankroes/ancestry) being one of the most popular. – engineersmnky Jun 16 '23 at 12:58

2 Answers2

2

You can use gem ancestry.

Please have a look at the official doc here

Add Gem in your gemfile

gem 'ancestry'

Create migration

rails g migration add_Add_ancestry_to_location ancestry:string:index

In you Location model add it as below:

class Location < ApplicationRecord
  has_ancestry
end

now you can use is as below:

location_a = Location.create(name: 'Location A')
location_b = Location.create(name: 'Location B', parent: location_a)
location_c = Location.create(name: 'Location C', parent: location_a)
location_d = Location.create(name: 'Location D', parent: location_b) 

parent_location = Location.find_by(name: 'Location A')
sublocations = parent_location.descendants

here sublocations will be an array with locations as Location B, Location C, Location D.

Amol Mohite
  • 603
  • 3
  • 10
0

If you are willing to make changes to your data model and are using Postgres perhaps you can take a quick look at pg_ltree

With this gem you can organise your locations using a ltree column on your locations table, and issue queries such as:

Location.find_by(path: "Earth.Europe")
Location.find_by(path: "Earth.Europe.France").root
Location.find_by(path: "Earth.Europe.France.Paris").children

If you need more advanced querying then you've come to the right place: ltree also supports regular expression matching which means you can write some pretty advanced query objects.

Also this will be quite fast because it offloads all the work to the database. Not saying you can't throw a cache in front of your queries anyway just to wow your PM and put a big smile on your DBA's face.

There are some downsides apparently related to handling of long strings as leaf names but you can always use some form of primary or secondary keys if your tree is tall.

Nick M
  • 2,424
  • 5
  • 34
  • 57