1

I have two related models, Trip and Ride. A Trip may have a going ride or a returning ride. My ActiveRecord models look like this:

class Route < ActiveRecord::Base
    has_one :trip
end

class Trip < ActiveRecord::Base
    belongs_to :going_route,     class_name: "Route"
    belongs_to :returning_route, class_name: "Route"
end

However this throws me an issue when I want to access a trip from a route:

Route.first.trip

This throws a PostgreSQL error:

PG::UndefinedColumn: ERROR: column trips.route_id does not exist

How can I tell to the Route class that his trip is under either going_route_id or returning_route_id? Or maybe there is another way around?

P.S: I've been looking for similar questions, there are a lot, but none is exactly like this one and solves my problem. If you have any tip on how to make the difference clearer, especially for the title. Here is some similar question


EDIT:

I've also tried using a lambda, as in matthew's duplicate proposition:

class FavoriteRoute < ActiveRecord::Base
    has_one :favorite_trip, -> (route) { where("going_route_id = :id OR returning_route_id = :id", id: route.id) }
end

This will throw the same error. And if I assume I should use find_by instead of where since I need only one result, I have another error which I really don't understand:

NoMethodError: undefined method `except' for #<Trip:0x00007f827b9d13e0>

Ulysse BN
  • 10,116
  • 7
  • 54
  • 82
  • Please be more specific as to how this isn't a duplicate of the linked question; I can't advise you on how to make the difference clearer, because I don't see one. – matthewd Aug 14 '18 at 13:56
  • `has_one :trip`, needs to mention the FK of the `Trip` model. or name it like `has_one :going_route_trip, class_name: "Trip"` and `has_one : returning_route_trip, class_name: "Trip"` – Arup Rakshit Aug 14 '18 at 13:57
  • @matthewd sorry I had a wrong link.. Updated now. For previous link, well they have a `has_many` relationship and I have a `has_one`, as well as many differencies, the accepted answer cannot fit to my issue. – Ulysse BN Aug 14 '18 at 14:10
  • Possible duplicate of [Rails association with multiple foreign keys](https://stackoverflow.com/questions/24642005/rails-association-with-multiple-foreign-keys) – matthewd Aug 14 '18 at 14:11
  • @ArupRakshit I've tried something similar, but route has ony one trip. So the foreign key may be either `returning_route_id` or `going_route_id`. I've tried to mention the key dynamically but this doesn't seems feasable. – Ulysse BN Aug 14 '18 at 14:12
  • @matthewd I've updated my question as well – Ulysse BN Aug 14 '18 at 14:25
  • I was looking more at the accepted answer. You can't define a relationship that points at two columns; you need to define two separate associations. – matthewd Aug 14 '18 at 14:27
  • Is there functionally any difference between a `going` and a `returning` route? Other than one is a route for going and the other is a route for returning. Do they behave differently? Or is it just a designation? – Jocko Aug 14 '18 at 15:55
  • @Jocko It is a designation only. – Ulysse BN Aug 14 '18 at 16:23

4 Answers4

2

You need to specify the foreign keys on the inverse side of the belongs_to association - meaning the has_one / has_many side that references the foreign key:

class Trip < ActiveRecord::Base
    # specifying foreign_key here is not needed since AR
    # will deduce that its outbound_route_id
    belongs_to :outbound_route,
      class_name: "Route"
    belongs_to :return_route, 
      class_name: "Route"
end

class Route < ActiveRecord::Base
  has_one :trip_as_outbound,
    class_name: 'Trip',
    foreign_key: :outbound_route_id
  has_one :trip_as_returning,
    class_name: 'Trip',
    foreign_key: :return_route_id

  def trip
    trip_as_outbound || trip_as_returning
  end
end

One way around this is to use Single Table Inheritance:

class Route < ApplicationRecord 
end

class Routes::Outbound < ::Route
  self.table_name = 'routes'
  has_one :trip, foreign_key: :outbound_route_id
end

class Routes::Return < ::Route
  self.table_name = 'routes'
  has_one :trip, foreign_key: :return_route_id
end

class Trip < ApplicationRecord
  belongs_to :outbound_route,
    class_name: '::Routes::Outbound'
  belongs_to :return_route,
    class_name: '::Routes::Return'
end

Which will give you the correct trip but has some strangeness such as that Routes::Return.all will give you the same result as Route.all.

This can be fixed by adding a type string column to the routes table. For performance add a compound index on type and id.

max
  • 96,212
  • 14
  • 104
  • 165
  • I'm sorry if my definition was not clear enough, I'm really struggling to get a concise question, but the model you are offering is far from representing our data: a route is, in fact, some polyline and time information, and a trip is the concatenation of two routes (and some other information). For your first answer, I will use this if I don't find another way around, but this will enforce the creation of a method `trip`, that will return whichever of both is not `nil`. – Ulysse BN Aug 14 '18 at 14:35
  • I found the naming and the model pretty confusing - so while it might not solve your exact domain problem the general principles still apply. `has_one / has_many` associations need to point to a single foreign key on the model with the `belongs_to` associations table. There really is no "dynamic" way around this as associations are declared on the class level - not per instance. – max Aug 14 '18 at 14:40
  • And `has_one :favorite_trip, -> (route) { where("going_route_id = :id OR returning_route_id = :id", id: route.id) }` will not work since the pickle is that the foreign key column is referenced in the JOIN clause. Tacking on a where clause does nothing to change that. – max Aug 14 '18 at 14:43
  • I added an alternative solution that borders on STI / polymorpism. – max Aug 14 '18 at 15:09
  • I guess STI is the way to go. Make sure you add that `type` column. If you have existing records make sure you update them and set the type column to `"Routes::Outbound"/"Routes::Return"`. – max Aug 14 '18 at 16:40
1

Being that there is no functional difference between a going and a returning route, consider setting up a has_many relationship between the trip and the route. This would make routes reusable for other trips, and get you what you're looking for.

NOTE: There are flaws in this approach, because you're using a many-to-many relationship. Which means one trip could have more than one going and/or returning route. You could manage this through code in the Trip model, or this may not be so bad, if you wanted to generate a "multi-stop" route for either direction.

You would generate a model called called trip_routes.

The trip_routes migration might look like this:

create_table :trip_routes do |t|
   t.integer :trip_id
   t.integer :route_id
   t.string  :route_type
   t.boolean :favorite
end

# Consider this part based on how you think your indexes are best built, I'm 
# just making note that DB performance can be impacted particularly on these
# two fields.
add_index :trip_routes, :trip_id
add_index :trip_routes, :route_id

Your trip_route model would look like this:

class TripRoute < ActiveRecord::Base
    belongs_to :trip
    belongs_to :route

    # This model knows whether it's the 'going' or 'returning' route, so do your 
    # route functionality here.
end

Then your trip model would look like this:

class Trip < ActiveRecord::Base
    has_many :trip_routes
    has_many :route, through: trip_routes

    # Helper to get the going route
    def going_trip_route
        self.trip_routes.find_by(route_type: "going")
    end

    # Helper to get the going route
    def returning_trip_route
        self.trip_routes.find_by(route_type: "returning")
    end

end

Your route model would look like this:

class Route < ActiveRecord::Base
    has_many :trip_routes
    has_many :trips, through: trip_routes
end
Jocko
  • 537
  • 2
  • 11
0

After seeing every possible solution, and our needs, we chose this next solution:

class Route
    def trip
        @trip ||= Trip.find_by("going_route_id = :id OR returning_route_id = :id", id: id)
    end
end

I don't think this is the best way around, and it feels hacky. However, this was the fastest to implement, without perf issue. The other issue with this solution is that there is no rails validation.

Ulysse BN
  • 10,116
  • 7
  • 54
  • 82
  • Is there a scenario where a route could be used for multiple trips? Seems very possible since you don't have an association from Route -> Trip that's managing the data. In that case, this would be problematic because it will only associate to the first Trip, and you will never see the association to other Trips. That's possibly a risk you're willing to take. – Jocko Aug 17 '18 at 17:55
  • Routes can only have one trip. However, we need to be able to quickly access route from a trip for performance issues (that is why `going_route_id` and `returning_route_id` where chosen) – Ulysse BN Aug 18 '18 at 11:52
0

try adding keys in routes table

add_column :routes, :going_key,     :integer
add_column :routes, :returning_key, :integer

then in your Trip and Route model

class Route < ActiveRecord::Base
  belongs_to :going_route, foreign_key: :going_key, class_name: Trip
  belongs_to :returning_route, foreign_key: :returning_key, class_name: Trip
end

class Trip < ActiveRecord::Base
end

Route.first.going_route 
Route.first.returning_route 
  • I don't think this works... For this, I would use one single `trip_id` key, and there would be an issue with the fact that both tables reference each other. Moreover, as said in the question, I need for performance issues to keep `going_route_id` and `returning_route_id` access from my `trips` table – Ulysse BN Aug 23 '18 at 13:30