Leveraging Ruby's Select in a ActiveRecord Serializer To Get Desired Nested Array
Published: 2023-04-05 23:45:01

Suppose that you want a nested array of data that fits a certain criteria rather than all the rows returned in a has_many and has_many through association.  How would one go about implementing a solution?  One way would be to use the Select method.

As an example, suppose you have a car application that has a user, car and dealer association in the following diagram.  You would like to get a nested array of a user, its dealers and ONLY cars for the specific user associated with a dealer instead of every user that has an association with the dealer. 

```console
users -< cars >- dealers
```
Following are the user, car and dealer models to further clarify the associations.

```console
class User < ApplicationRecord

    has_many :cars
    has_many :dealers, through: :cars
 
end
```

```console
class Car < ApplicationRecord

  belongs_to :user
  belongs_to :dealer
  
end
```

```console
class Dealer < ApplicationRecord
    
    has_many :cars
    has_many :users, through: :cars

end
```
If we make a show call to the User and a User Serializer with just User columns, you will simply get a User hash with User data.  But, we want a return nested array of a user, its dealers and ONLY the user's cars.

Let's make the following change to our User serializer.

```console
class UserSerializer < ActiveModel::Serializer
  attributes :id, :username, :first_name, :last_name, 
                         :dealers_with_cars 

  def dealers_with_cars
      byebug  
  end

end
```
 
We will now add a method "dealers_with_cars" to our show serializer as shown above.  When we now run our show query, we will be stopped at the byebug.  At the byebug, query "self.object.dealers".  This query will retrieve all the dealers associated with the user.  

We have found a way to get our first nested array of the User's dealers.  Now we want to get another nested array of cars that will hold only cars that belong to specified user rather that all of the dealer's cars. 

If we now map through our dealers (or "self.object.dealers"), we can now get access to each dealer's cars.  If we now change our User serializer to the following we get every user's cars associated with this dealer.

```console
class UserSerializer < ActiveModel::Serializer
  attributes :id, :username, :first_name, :last_name, 
                     :dealers_with_cars 

  def dealers_with_cars
     self.object.dealers.map do |dealer|
        dlr = {}
        dlr[:id] = dealer.id
        dlr[:name] = dealer.name
        dlr[:contact] = dealer.contact
        dlr[:phone] = dealer.phone
        dlr[:email] = dealer.email
        dlr[:cars] = dealer.cars
        dlr
     end  
  end

end
```

If we now run our User show query, we get the following results.  As you can see, it includes cars that do NOT belong to the specific user which is not what we want as you can see below.
 
```console
{
"id": 5,
"username": "craig",
"first_name": "Craig",
"last_name": "Ford",
"dealers_with_cars": [
    {
       "id": 13,
       "name": "Baja Motors",
       "contact": "Bryan Knotts",
       "phone": "6095288443",
       "email": "b_knotts@baja.com",
       "cars": [
          {
                 "user_id": 5,
                 "dealer_id": 13,
                 "year": 1989,
                 "make": "Datsun",
                 "model": "210",
          },
          {
                 "user_id": 6,
                 "dealer_id": 13,
                 "year": 2006,
                 "make": "Ford",
                 "model": "Escort",
          },
          {
                 "user_id": 21,
                 "dealer_id": 13,
                 "year": 2020,
                 "make": "Nissan",
                 "model": "Spirit",
          }
       ]
    },
    {
       "id": 19,
       "name": "Stagger Inn",
       "contact": "John Moos",
       "phone": "8127864433",
       "email": "jmoos@stagger.com",
       "cars": [
          {
                 "user_id": 5,
                 "dealer_id": 19,
                 "year": 2010,
                 "make": "Mercedes",
                 "model": "GL 450",
          },
          {
                 "user_id": 11,
                 "dealer_id": 19,
                 "year": 2016,
                 "make": "Ford",
                 "model": "Escort",
          }
       ]
    }
  ]
}
```
Fortunately, we only need to add the Array Select method to our Cars array to get our desired result.  If we change the User to the following, we will get the desired results.

```console
class UserSerializer < ActiveModel::Serializer
  attributes :id, :username, :first_name, :last_name, :dealers_with_cars 

  def dealers_with_cars
     self.object.dealers.map do |dealer|
        dlr = {}
        dlr[:id] = dealer.id
        dlr[:name] = dealer.name
        dlr[:contact] = dealer.contact
        dlr[:phone] = dealer.phone
        dlr[:email] = dealer.email
        dlr[:cars] = dealer.cars.select {|car| car.user_id == 
                                               self.object.id}
        dlr
     end  
  end

end
```

We now get our desired results.

```console
{
"id": 5,
"username": "craig",
"first_name": "Craig",
"last_name": "Ford",
"dealers_with_cars": [
    {
       "id": 13,
       "name": "Baja Motors",
       "contact": "Bryan Knotts",
       "phone": "6095288443",
       "email": "b_knotts@baja.com",
       "cars": [
          {
                 "user_id": 5,
                 "dealer_id": 13,
                 "year": 1989,
                 "make": "Datsun",
                 "model": "210",
          },
          {
                 "user_id": 6,
                 "dealer_id": 13,
                 "year": 2006,
                 "make": "Ford",
                 "model": "Escort",
          }
       ]
    },
    {
       "id": 19,
       "name": "Stagger Inn",
       "contact": "John Moos",
       "phone": "8127864433",
       "email": "jmoos@stagger.com",
       "cars": [
          {
                 "user_id": 5,
                 "dealer_id": 19,
                 "year": 2010,
                 "make": "Mercedes",
                 "model": "GL 450",
          }
       ]
    }
  ]
}
```

Thanks for reading!