Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/chaadow/active_record-acts_as
Simulate multi-table inheritance for ActiveRecord models
https://github.com/chaadow/active_record-acts_as
activerecord inheritance rails
Last synced: 3 days ago
JSON representation
Simulate multi-table inheritance for ActiveRecord models
- Host: GitHub
- URL: https://github.com/chaadow/active_record-acts_as
- Owner: chaadow
- License: mit
- Created: 2017-03-07T07:12:48.000Z (almost 8 years ago)
- Default Branch: master
- Last Pushed: 2024-06-18T00:35:15.000Z (7 months ago)
- Last Synced: 2025-01-11T17:08:30.059Z (10 days ago)
- Topics: activerecord, inheritance, rails
- Language: Ruby
- Homepage: https://chaadow.github.io/active_record-acts_as/
- Size: 302 KB
- Stars: 99
- Watchers: 3
- Forks: 4
- Open Issues: 5
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE.txt
Awesome Lists containing this project
README
# ActiveRecord::ActsAs
![Gem](https://img.shields.io/gem/v/active_record-acts_as?style=for-the-badge)
![Build Status](https://img.shields.io/github/actions/workflow/status/chaadow/active_record-acts_as/ruby.yml?style=for-the-badge)Simulates multiple-table-inheritance (MTI) for ActiveRecord models.
By default, ActiveRecord only supports single-table inheritance (STI).
MTI gives you the benefits of STI but without having to place dozens of empty fields into a single table.Take a traditional e-commerce application for example:
A product has common attributes (`name`, `price`, `image` ...),
while each type of product has its own attributes:
for example a `pen` has `color`, a `book` has `author` and `publisher` and so on.
With multiple-table-inheritance you can have a `products` table with common columns and
a separate table for each product type, i.e. a `pens` table with `color` column.## Requirements
* Ruby >= `2.7`
* ActiveSupport >= `6.0` ( supports `main`/edge branch )
* ActiveRecord >= `6.0` ( supports `main`/edge branch )## Installation
Add this line to your application's Gemfile:
gem 'active_record-acts_as'
And then execute:
$ bundle
Or install it yourself as:
$ gem install active_record-acts_as
## Usage
Back to example above, all you have to do is to mark `Product` as `actable` and all product type models as `acts_as :product`:
```ruby
class Product < ActiveRecord::Base
actable
belongs_to :storevalidates_presence_of :name, :price
def info
"#{name} $#{price}"
end
endclass Pen < ActiveRecord::Base
acts_as :product
endclass Book < ActiveRecord::Base
# In case you don't wish to validate
# this model against Product
acts_as :product, validates_actable: false
endclass Store < ActiveRecord::Base
has_many :products
end
```and add foreign key and type columns to products table as in a polymorphic relation.
You may prefer using a migration:```ruby
change_table :products do |t|
t.integer :actable_id
t.string :actable_type
end
```or use shortcut `actable`
```ruby
change_table :products do |t|
t.actable
end
```**Make sure** that column names do not match on parent and subclass tables,
that will make SQL statements ambiguous and invalid!
Specially **DO NOT** use timestamps on subclasses, if you need them define them
on parent table and they will be touched after submodel updates (You can use the option `touch: false` to skip this behaviour).Now `Pen` and `Book` **acts as** `Product`, i.e. they inherit `Product`s **attributes**,
**methods** and **validations**. Now you can do things like these:```ruby
Pen.create name: 'Penie!', price: 0.8, color: 'red'
# => #
Pen.where price: 0.8
# => [#]# You can seamlessly query Product attributes
pen = Pen.where(name: 'new pen', color: 'black').first_or_initialize
# => #
pen.name
# => "new pen"# You can also call `exists?` using Product attributes:
Pen.exists?(name: 'Penie!', price: 0.8)
# => trueProduct.where price: 0.8
# => [#]
pen = Pen.new
pen.valid?
# => false
pen.errors.full_messages
# => ["Name can't be blank", "Price can't be blank", "Color can't be blank"]
Pen.first.info
# => "Penie! $0.8"
```On the other hand you can always access a specific object from its parent by calling `specific` method on it:
```ruby
Product.first.specific
# => #
```If you have to come back to the parent object from the specific, the `acting_as` returns the parent element:
```ruby
Pen.first.acting_as
# => #
```Likewise, `actables` converts a relation of specific objects to their parent objects:
```ruby
Pen.where(...).actables
# => [#, ...]
```In `has_many` case you can use subclasses:
```ruby
store = Store.create
store.products << Pen.create
store.products.first
# => #
```You can give a name to all methods in `:as` option:
```ruby
class Product < ActiveRecord::Base
actable as: :producible
endclass Pen < ActiveRecord::Base
acts_as :product, as: :producible
endchange_table :products do |t|
t.actable as: :producible
end
````acts_as` support all `has_one` options, where defaults are there:
`as: :actable, dependent: :destroy, validate: false, autosave: true`Make sure you know what you are doing when overwriting `validate` or `autosave` options.
You can pass scope to `acts_as` as in `has_one`:
```ruby
acts_as :person, -> { includes(:friends) }
````actable` support all `belongs_to` options, where defaults are these:
`polymorphic: true, dependent: :destroy, autosave: true`Make sure you know what you are doing when overwriting `polymorphic` option.
### Namespaced models
If your `actable` and `acts_as` models are namespaced, you need to configure them like this:
```ruby
class MyApp::Product < ApplicationRecord
actable inverse_of: :product
endclass MyApp::Pen < ApplicationRecord
acts_as :product, class_name: 'MyApp::Product'
end
```## Caveats
Multiple `acts_as` in the same class are not supported!
## RSpec custom matchersTo use this library custom RSpec matchers, you must require the `rspec/acts_as_matchers` file.
Examples:
```ruby
require "active_record/acts_as/matchers"RSpec.describe "Pen acts like a Product" do
it { is_expected.to act_as(:product) }
it { is_expected.to act_as(Product) }it { expect(Person).to act_as(:product) }
it { expect(Person).to act_as(Product) }
endRSpec.describe "Product is actable" do
it { expect(Product).to be_actable }
end
```## Contributing
1. Fork it (https://github.com/chaadow/active_record-acts_as/fork)
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Test changes don't break anything (`rspec`)
4. Add specs for your new feature
5. Commit your changes (`git commit -am 'Add some feature'`)
6. Push to the branch (`git push origin my-new-feature`)
7. Create a new Pull Request