Problem:
In a nutshell, you have 2 questions. First, you are trying to preload more than you need (?!?), And the other is that Rails does not want to load what you really need, due to the recursive nature of the logic.
To clarify in more detail, consider the following:
my_action.parents.map(&:parents).flatten.map(&:parents)
The rails will be:
- first take all parents for this action.
- then collapse each of these parents and take your parents.
- then smooth these โgrandparentsโ into an array, move on to each of them and bring them to your parents.
Please note that in this case there is not much point in loading the first level parents, since you are just starting with an Action instance, not a collection. The .parents call will display all first-level parents for this action in one go (which would require a download).
So what happens when you start with a collection (ActiveRelation) instead of an instance?
Action.some_scope.includes(:parents).map(&:parents)
In this case, the parents of ALL actions included in the scope will be loaded. Calling .map(&:parents) will NOT trigger further SQL calls, and thatโs the whole point of loading with includes() . However, there are two things that hit the whole purpose of this - and you do both of them: /
First, your starting point is not really a set of actions, since you immediately call .last . Thus, the choice of all parents for ALL actions is meaningless - you only need the "last"! Because of this, Rails is smart enough to scale down and will only load parents from the "last" action. However, in this case there was not much benefit from impatient loading, since calling .parents would lead to the same request at a later time. (Although there is little benefit to downloading in advance if subsequent operations should be faster, which is of limited utility in this case). Thus, with or without the .includes statement, you would execute a single request to retrieve the parents for the last action.
More importantly, you recursively call .parents for each of these parents, and Rails was completely unaware that you were going to do this. Moreover, recursive calls are not preliminary in nature (without knowing a few tricks), so there is no way to tell ActiveRecord or use 'vanilla' SQL in this regard to go through the chain and find out which parents are not required until then as it is already done that (by making a moot point). All this leads to a nightmare of the situation N + 1, how you worry.
Some solutions:
There are several ways to mitigate or eliminate the N + 1 problem - in order of complexity of implementation:
- Pre-fetch to N parental levels (assuming you know what max (N) is)
Action.last.parents.includes(parents: {parents: :parents}) # grabs up to 4 levels
Skip SQL completely, load all the actions into the hash of the Action arrays associated with the child_id, and use non-ActiveRecord methods to aggregate what you need using simple Ruby. This will deteriorate rapidly as your data grows, but it can be good enough for you - at least for now.
Use a diagram that allows you to predefine the ancestor tree and use this utility to help with this calculation. The example provided by @bbozo is one way to do this - you can also explore gems such as ancestors, act_as_tree, awesome_nested_set, clos_tree and possibly others to help you with this.
Use a database-specific function that actually performs recursive calculation in one call. PostgreSQL, Oracle, and MS-SQL have this capability, while MySQL and SQLite do not. This will probably give you better performance, but can be difficult to write using only the ActiveRecord query interface.