https://github.com/mitigate-dev/periodic_records
Support functions for ActiveRecord models with periodic entries
https://github.com/mitigate-dev/periodic_records
Last synced: 5 months ago
JSON representation
Support functions for ActiveRecord models with periodic entries
- Host: GitHub
- URL: https://github.com/mitigate-dev/periodic_records
- Owner: mitigate-dev
- License: mit
- Created: 2015-09-30T08:15:50.000Z (over 10 years ago)
- Default Branch: master
- Last Pushed: 2024-02-21T13:32:41.000Z (over 2 years ago)
- Last Synced: 2026-01-13T06:20:47.241Z (6 months ago)
- Language: Ruby
- Homepage:
- Size: 40 KB
- Stars: 7
- Watchers: 8
- Forks: 2
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE.txt
Awesome Lists containing this project
README
# PeriodicRecords
Support functions for ActiveRecord models with periodic entries.
* Supports periods where the smallest unit is a whole day
* Adjusts and splits overlapping records
* Preloads currently active records to avoid N+1 queries
* Easy querying within history - join returns 0..1 records (no grouping needed)
`LEFT JOIN ... ON ... AND BETWEEN start_at AND end_at`
For example you have employees table and assignments table that stores all the
employment history.
Employees:
| id | name |
|:---|:-----|
| 1 | John |
Employee assignments:
| id | employee_id | start_at | end_at | job_title |
|:---|:------------|:-----------|:-----------|:----------|
| 1 | 1 | 2014-01-01 | 9999-01-01 | Developer |
Now John is promoted to "Senior Developer" and you create a new employee
assignment record and this gem will take care of adjusting and splitting
overlapping records. In this case it will adjust the `end_at` field for the
previous assignment.
| id | employee_id | start_at | end_at | job_title |
|:---|:------------|:-----------|:-----------|:-----------------|
| 1 | 1 | 2014-01-01 | 2018-05-04 | Developer |
| 2 | 1 | 2018-05-05 | 9999-01-01 | Senior Developer |
## Installation
Add this line to your application's Gemfile:
```ruby
gem 'periodic_records'
```
And then execute:
```bash
$ bundle
```
Or install it yourself as:
```bash
$ gem install periodic_records
```
## Preparation
Ensure `start_at` and `end_at` date columns on the model that will have
periodic versions.
Include `PeriodicRecords::Model` and define `siblings` method:
```ruby
class EmployeeAssignment < ActiveRecord::Base
include PeriodicRecords::Model
belongs_to :employee
def siblings
self.class.where(employee_id: employee_id).where.not(id: id)
end
end
```
Include `PeriodicRecords::Associations` in the model that has periodic
associations, and call `has_periodic`:
```ruby
class Employee < ActiveRecord::Base
include PeriodicRecords::Associations
has_many :employee_assignments, inverse_of: :employee
has_periodic :employee_assignments, as: :assignments
end
```
## Usage
Look up the currently active record with `model.current_association`:
```ruby
employee.current_assignment
```
Look up records for specific date or period
with `within_date` and `within_interval`:
```ruby
employee.employee_assignments.within_date(Date.tomorrow)
```
```ruby
employee.employee_assignments.within_interval(Date.current.beginning_of_month, Date.current.end_of_month)
```
Look up records starting with specific date with `from_date`
```ruby
employee.employee_assignments.from_date(Date.tomorrow)
```
Preload currently active records, to avoid N+1 queries on `current_assignment`.
```ruby
employees = Employee.all
Employee.preload_current_assignments(employees)
employees.each do |employee|
puts employee.current_assignment.to_s
end
```
## Database Constraints
To avoid inconsistent data in race conditions, you can add database constraint
that checks overlapping periods.
Postgres:
```ruby
class AddEmployeeAssignmentsOverlappingDatesConstraint < ActiveRecord::Migration
def up
execute "CREATE EXTENSION IF NOT EXISTS btree_gist"
execute <<-SQL
ALTER TABLE employee_assignments
ADD CONSTRAINT employee_assignments_overlapping_dates
EXCLUDE USING GIST(
employee_id WITH =,
DATERANGE(start_at, end_at, '[]') WITH &&
)
SQL
end
def down
execute <<-SQL.squish
ALTER TABLE employee_assignments
DROP CONSTRAINT employee_assignments_overlapping_dates
SQL
end
end
```
## Time sensitive records
If you need your records to be split with time component, then set `start_at` and `end_at` columns to `datetime` type.
Use `TSRANGE` instead of `DATERANGE` when creating database constraint.
## Gapless records
If you want to avoid gaps between records, you can include also `PeriodicRecords::Gapless`.
```ruby
class EmployeeAssignment < ActiveRecord::Base
include PeriodicRecords::Model
include PeriodicRecords::Gapless
belongs_to :employee
def siblings
self.class.where(employee_id: employee_id).where.not(id: id)
end
end
```
Example:
| id | employee_id | start_at | end_at | job_title |
|:---|:------------|:-----------|:-----------|:-----------------|
| 1 | 1 | 0001-01-01 | 2018-01-15 | Junior Developer |
| 2 | 1 | 2018-01-16 | 2018-02-15 | Developer |
| 3 | 1 | 2018-02-16 | 9999-01-01 | Senior Developer |
If you update #2 from `2018-01-16 - 2018-02-15` to `2018-01-20 - 2018-02-10`, it
will also adjust end at for #1 and start at for #3 to avoid gaps between records.
After (with `Gapless`):
| id | employee_id | start_at | end_at | job_title |
|:---|:------------|:-----------|:-----------|:-----------------|
| 1 | 1 | 0001-01-01 | 2018-01-19 | Junior Developer |
| 2 | 1 | 2018-01-20 | 2018-02-10 | Developer |
| 3 | 1 | 2018-02-11 | 9999-01-01 | Senior Developer |
After (without `Gapless`):
| id | employee_id | start_at | end_at | job_title |
|:---|:------------|:-----------|:-----------|:-----------------|
| 1 | 1 | 0001-01-01 | 2018-01-15 | Junior Developer |
| 2 | 1 | 2018-01-20 | 2018-02-10 | Developer |
| 3 | 1 | 2018-02-16 | 9999-01-01 | Senior Developer |
If you delete #2 then it will adjust end at for #1.
You will not be able to delete entry that is at the beginning (#1) or at the end (#3).
You will not be able to adjust start at for the beginning entry (#1).
You will not be able to adjust end at for the ending entry (#3).
For more examples see [gapless_spec.rb](spec/periodic_records/gapless_spec.rb).
## Development
After checking out the repo, run `bin/setup` to install dependencies.
Then, run `bin/console` for an interactive prompt that will allow you to
experiment.
To install this gem onto your local machine, run `bundle exec rake install`.
To release a new version, update the version number in `version.rb`,
and then run `bundle exec rake release` to create a git tag for the version,
push git commits and tags, and push the `.gem` file
to [rubygems.org](https://rubygems.org).
## Contributing
1. Fork it ( https://github.com/mak-it/periodic_records/fork )
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request