benjaminjosephw
benjaminjosephw

Reputation: 4417

Can I have a one way HABTM relationship?

Say I have the model Item which has one Foo and many Bars.

Foo and Bar can be used as parameters when searching for Items and so Items can be searched like so:

www.example.com/search?foo=foovalue&bar[]=barvalue1&bar[]=barvalue2

I need to generate a Query object that is able to save these search parameters. I need the following relationships:

I have this relationship set up currently like so:

class Query < ActiveRecord::Base
  belongs_to :foo
  has_and_belongs_to_many :bars
  ...
end

Query also has a method which returns a hash like this: { foo: 'foovalue', bars: [ 'barvalue1', 'barvalue2' } which easily allows me to pass these values into a url helper and generate the search query.

This all works fine.

My question is whether this is the best way to set up this relationship. I haven't seen any other examples of one-way HABTM relationships so I think I may be doing something wrong here.

Is this an acceptable use of HABTM?

Upvotes: 1

Views: 549

Answers (1)

fny
fny

Reputation: 33587

Functionally yes, but semantically no. Using HABTM in a "one-sided" fashion will achieve exactly what you want. The name HABTM does unfortunately insinuate a reciprocal relationship that isn't always the case. Similarly, belongs_to :foo makes little intuitive sense here.

Don't get caught up in the semantics of HABTM and the other association, instead just consider where your IDs need to sit in order to query the data appropriately and efficiently. Remember, efficiency considerations should above all account for your productivity.

I'll take the liberty to create a more concrete example than your foos and bars... say we have an engine that allows us to query whether certain ducks are present in a given pond, and we want to keep track of these queries.

Possibilities

You have three choices for storing the ducks in your Query records:

  1. Join table
  2. Native array of duck ids
  3. Serialized array of duck ids

You've answered the join table use case yourself, and if it's true that "neither [Duck] nor [Pond] need to know anything about Query", using one-sided associations should cause you no problems. All you need to do is create a ducks_queries table and ActiveRecord will provide the rest. You could even opt to use has_many :through relationship if you need to do anything fancy.

At times arrays are more convenient than using join tables. You could store the data as a serialized integer array and add handlers for accessing the data similar to the following:

class Query
 serialize :duck_ids
 def ducks
   transaction do 
     Duck.where(id: duck_ids)
   end
 end
end

If you have native array support in your database, you can do the same from within your DB. similar.

With Postgres' native array support, you could make a query as follows:

SELECT * FROM ducks WHERE id=ANY(
  (SELECT duck_ids FROM queries WHERE id=1 LIMIT 1)::int[]
)

You can play with the above example on SQL Fiddle

Trade Offs

  1. Join table:
    • Pros: Convention over configuration; You get all the Rails goodies (e.g. query.bars, query.bars=, query.bars.where()) out of the box
    • Cons: You've added complexity to your data layer (i.e. another table, more dense queries); makes little intuitive sense
  2. Native array:
    • Pros: Semantically nice; you get all the DB's array-related goodies out of the box; potentially more performant
    • Cons: You'll have to roll your own Ruby/SQL or use an ActiveRecord extension such as postgres_ext; not DB agnostic; goodbye Rails goodies
  3. Serialized array:
    • Pros: Semantically nice; DB agnostic
    • Cons: You'll have to roll your own Ruby; you'll loose the ability to make certain queries directly through your DB; serialization is icky; goodbye Rails goodies

At the end of the day, your use case makes all the difference. That aside, I'd say you should stick with your "one-sided" HABTM implementation: you'll lose a lot of Rails-given gifts otherwise.

Upvotes: 3

Related Questions