Low commit activity in last 3 years
No release in over a year
A small library that helps with models which can have multiple parent model types
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

Runtime

>= 5.2, < 8
>= 5.2, < 8
 Project Readme

belongs_to_one_of

Gem to support activemodel relations where one model can be a child of one of many models. In our examples, we will be targeting a class Competitor which can either belong to a School or a College. We will consider this more general concept an organisation.

The gem provides a simple method to declare this relationship, some validators to enforce the relationships, and some helper functions to safely set and get the associated model.

What about rails polymorphic relations?

Unlike rails polymorphic relations, this supports having a separate id column for each parent model type (e.g. school_id and college_id instead of just organisation_id). This is desirable (in some cases) to enable the database to use foreign keys.

The gem will also error if you try to set a resource which isn't one of the specified classes, unlike a rails polymorphic relation which will accept any model class.

Installation

Install the package from Rubygems:

gem install belongs_to_one_of

Or add it to your gemfile

gem 'belongs_to_one_of'

For our policy on compatibility with Ruby versions, see COMPATIBILITY.md.

Quick Start

Our code will say:

This model belongs to an organisation, which might be a School or might be a College

This allows us to store the association in the columns school_id and college_id, which can use foreign keys, but for the 99% of your code which doesn't care which is which, they can just call a method organisation or organisation_id to access the resource.

The library adds a new association to ActiveRecord called belongs_to_one_of :organisation, . To use it, simply call this hook in your ActiveRecord class:

class Competitor < ActiveRecord::Base
  belongs_to_one_of :organisation, %i[school college]
end

class School < ActiveRecord::Base
  has_many :competitors
end 

class College < ActiveRecord::Base
  has_many :competitors
end 

school = School.new

my_competitor = Competitor.create(name: 'jack', organisation: school)

my_competitor.organisation
# => school

my_competitor.organisation_id == school.id
# => true 

Note that this helper calls belongs_to :school, optional:true and belongs_to :college, optional:true, so you don't have to.

Available methods

The hook defines a few methods on your class. The names are dynamic, we will use 'organisation' as our example (as per above):

Validators

belongs_to_exactly_one_[organisation]

A validator that can be used to check that a model belongs to exactly one organisation

belongs_to_at_most_one_[organisation]

A validator that can be used to check that a model belongs to either no organisations or one organisation

[organisation_type]_matches_[organisation]

A validator that can be used to check that the type of model matches the model. Only relevant when include_type_column is true

Getters & Setters

[organisation]=

Allows you to create a new instance of the model with the interface:

Competitor.new(
  organisation: my_school,
  name: "Joe Bloggs"
)

This will raise a ModelNotFound exception if the organisation is not one of the permitted model types

[organisation]

Allows you to get the linked resource via .organisation e.g.:

my_competitor.organisation

[organisation]_id

Allows you to get the linked resource's id via .organisation_id e.g.:

my_competitor.organisation_id

[organisation]_type

This is only set when the associations are configured with include_type_column (see below). This allows you to access the resource type via .organisation_type e.g.:

my_competitor.organisation_type
# => 'School'

Configuration Options

include_type_column

By default, the library assumes that the underlying table looks like:

id name school_id college_id
1 Aaron J Aaronson COL123
2 Betty F Parker SCH456

however you can set include_type_column: true to explicitly store what type of model is connected, e.g.:

  belongs_to_one_of :organisation, %i[school college], include_type_column: true
id name organisation_type school_id college_id
1 Aaron J Aaronson College COL123
2 Betty F Parker School SCH456

if the column is not called [organisation]_type, you can specify the column name e.g.

  belongs_to_one_of :organisation, %i[school college], include_type_column: :type_of_organisation
id name type_of_organisation school_id college_id
1 Aaron J Aaronson College COL123
2 Betty F Parker School SCH456

type_column_value

If you have include_type_column: true set, by default we assume you want to store the classname in the db. However, there may be some logic that you want to apply. If you pass a Proc to type_column_value you can add your own logic to determine what goes into the db.

  belongs_to_one_of :organisation,  %i[school college], include_type_column: true,
                                                       type_column_value: ->(resource) { resource.class.downcase }
id name organisation_type school_id college_id
1 Aaron J Aaronson college COL123
2 Betty F Parker school SCH456

License & Contributing

GoCardless ♥ open source. If you do too, come join us.