Need help with datamappify?
Click the “chat” button below for chat support from the developer who created it, or find similar developers for support.

About the developer

fredwu
338 Stars 19 Forks 302 Commits 3 Opened issues

Description

Compose, decouple and manage domain logic and data persistence separately. Works particularly great for composing form objects!

Services available

!
?

Need anything else?

Contributors list

# 20,558
Ruby
Elixir
Rails
Sass
267 commits
# 358,776
HTML
sequel
Rust
reposit...
5 commits
# 261,143
Shell
reposit...
express...
GraphQL
3 commits
# 152,485
macOS
CSS
Xcode
Markdow...
3 commits
# 24,228
Ruby
sqlite3
oracle
SQL
1 commit
# 9,793
autohot...
ahk
bitwise...
Nette
1 commit

Datamappify is no longer being maintained. It started off with a noble goal, unfortunately due to it being on the critical path of our project, we have decided not to continue developing it given the lack of development time from me.

Feel free to read the README and browse the code, I still believe in the solutions for this particular domain.

For a more active albeit still young project, check out Lotus::Model.


Datamappify Gem Version Build Status Coverage Status Code Climate

Compose, decouple and manage domain logic and data persistence separately. Works particularly great for composing form objects!

Overview

The typical Rails (and ActiveRecord) way of building applications is great for small to medium sized projects, but when projects grow larger and more complex, your models too become larger and more complex - it is not uncommon to have god classes such as a User model.

Datamappify tries to solve two common problems in web applications:

  1. The coupling between domain logic and data persistence.
  2. The coupling between forms and models.

Datamappify is loosely based on the Repository Pattern and Entity Aggregation, and is built on top of Virtus and existing ORMs (ActiveRecord and Sequel, etc).

There are three main design goals:

  1. To utilise the powerfulness of existing ORMs so that using Datamappify doesn't interrupt too much of your current workflow. For example, Devise would still work if you use it with a
    UserAccount
    ActiveRecord model that is attached to a
    User
    entity managed by Datamappify.
  2. To have a flexible entity model that works great with dealing with form data. For example, SimpleForm would still work with nested attributes from different ORM models if you map entity attributes smartly in your repositories managed by Datamappify.
  3. To have a set of data providers to encapsulate the handling of how the data is persisted. This is especially useful for dealing with external data sources such as a web service. For example, by calling
    UserRepository.save(user)
    , certain attributes of the user entity are now persisted on a remote web service. Better yet, dirty tracking and lazy loading are supported out of the box!

Datamappify consists of three components:

  • Entity contains models behaviour, think an ActiveRecord model with the persistence specifics removed.
  • Repository is responsible for data retrieval and persistence, e.g.
    find
    ,
    save
    and
    destroy
    , etc.
  • Data as the name suggests, holds your model data. It contains ORM objects (e.g. ActiveRecord models).

Below is a high level and somewhat simplified overview of Datamappify's architecture.

Note: Datamappify is NOT affiliated with the Datamapper project.

Built-in ORMs for Persistence

You may implement your own data provider and criteria, but Datamappify comes with build-in support for the following ORMS:

  • ActiveRecord
  • Sequel

Requirements

  • ruby 2.0+
  • ActiveModel 4.0+

Installation

Add this line to your application's Gemfile:

gem 'datamappify'

Usage

Entity

Entity uses Virtus DSL for defining attributes and ActiveModel::Validations DSL for validations.

The cool thing about Virtus is that all your attributes get coercion for free!

Below is an example of a User entity, with inline comments on how some of the DSLs work.

class User
  include Datamappify::Entity

attribute :first_name, String attribute :last_name, String attribute :age, Integer attribute :passport, String attribute :driver_license, String attribute :health_care, String

Nested entity composition - composing the entity with attributes and validations from other entities

class Job

include Datamappify::Entity

attributes :title, String

validates :title, :presence => true

end

class User

# ...

attributes_from Job

end

essentially equals:

class User

# ...

attributes :title, String

validates :title, :presence => true

end

attributes_from Job

optionally you may prefix the attributes, so that:

class Hobby

include Datamappify::Entity

attributes :name, String

validates :name, :presence => true

end

class User

# ...

attributes_from Hobby, :prefix_with => :hobby

end

becomes:

class User

# ...

attributes :hobby_name, String

validates :hobby_name, :presence => true

end

attributes_from Hobby, :prefix_with => :hobby

Entity reference

references is a convenient method for:

attribute :account_id, Integer

attr_accessor :account

and it assigns account_id the correct value:

user.account = account #=> user.account_id = account.id

references :account

validates :first_name, :presence => true, :length => { :minimum => 2 } validates :passport, :presence => true, :length => { :minimum => 8 }

def full_name "#{first_name} #{last_name}" end end

Entity inheritance

Inheritance is supported for entities, for example:

class AdminUser < User
  attribute :level, Integer
end

class GuestUser < User attribute :expiry, DateTime end

Lazy loading

Datamappify supports attribute lazy loading via the

Lazy
module.
class User
  include Datamappify::Entity
  include Datamappify::Lazy
end

When an entity is lazy loaded, only attributes from the primary source (e.g.

User
entity's primary source would be
ActiveRecord::User
as specified in the corresponding repository) will be loaded. Other attributes will only be loaded once they are called. This is especially useful if some of your data sources are external web services.

Repository

Repository maps entity attributes to DB columns - better yet, you can even map attributes to different ORMs!

Below is an example of a repository for the User entity, you can have more than one repositories for the same entity.

class UserRepository
  include Datamappify::Repository

specify the entity class

for_entity User

specify the default data provider for unmapped attributes

optionally you may use Datamappify.config to config this globally

default_provider :ActiveRecord

specify any attributes that need to be mapped

for attributes mapped from a different source class, a foreign key on the source class is required

for example:

- 'last_name' is mapped to the 'User' ActiveRecord class and its 'surname' attribute

- 'driver_license' is mapped to the 'UserDriverLicense' ActiveRecord class and its 'number' attribute

- 'passport' is mapped to the 'UserPassport' Sequel class and its 'number' attribute

- attributes not specified here are mapped automatically to 'User' with provider 'ActiveRecord'

map_attribute :last_name, :to => 'User#surname' map_attribute :driver_license, :to => 'UserDriverLicense#number' map_attribute :passport, :to => 'UserPassport#number', :provider => :Sequel map_attribute :health_care, :to => 'UserHealthCare#number', :provider => :Sequel

alternatively, you may group attribute mappings if they share certain options:

group :provider => :Sequel do map_attribute :passport, :to => 'UserPassport#number' map_attribute :health_care, :to => 'UserHealthCare#number' end

attributes can also be reverse mapped by specifying the via option

for example, the below attribute will look for hobby_id on the user object,

and map hobby_name from the name attribute of ActiveRecord::Hobby

this is useful for mapping form fields (similar to ActiveRecord's nested attributes)

map_attribute :hobby_name, :to => 'Hobby#name', :via => :hobby_id

by default, Datamappify maps attributes using an inferred reference (foreign) key,

for example, the first mapping below will look for the user_id key in Bio,

the second mapping below will look for the person_id key in Bio instead

map_attribute :bio, :to => 'Bio#body' map_attribute :bio, :to => 'Bio#body', :reference_key => :person_id end

Repository inheritance

Inheritance is supported for repositories when your data structure is based on STI (Single Table Inheritance), for example:

class AdminUserRepository < UserRepository
  for_entity AdminUser
end

class GuestUserRepository < UserRepository for_entity GuestUser

map_attribute :expiry, :to => 'User#expiry_date' end

In the above example, both repositories deal with the

ActiveRecord::User
data model.

Override mapped data models

Datamappify repository by default creates the underlying data model classes for you. For example:

map_attribute :driver_license, :to => 'UserData::DriverLicense#number'

In the above example, a

Datamppify::Data::Record::ActiveRecord::UserDriverLicense
ActiveRecord model will be created. If you would like to customise the data model class, you may do so by creating one either under the default namespace or under the
Datamappify::Data::Record::NameOfDataProvider
namespace:
module UserData
  class DriverLicense < ActiveRecord::Base
    # your customisation...
  end
end
module Datamappify::Data::Record::ActiveRecord::UserData
  class DriverLicense < ::ActiveRecord::Base
    # your customisation...
  end
end

Repository APIs

More repository APIs are being added, below is a list of the currently implemented APIs.

Retrieving an entity

Accepts an id.

user = UserRepository.find(1)

Checking if an entity exists in the repository

Accepts an entity.

UserRepository.exists?(user)

Retrieving all entities

Returns an array of entities.

users = UserRepository.all

Searching entities

Returns an array of entities.

Simple
users = UserRepository.where(:first_name => 'Fred', :driver_license => 'AABBCCDD')
Match
users = UserRepository.match(:first_name => 'Fre%', :driver_license => '%bbcc%')
Advanced

You may compose search criteria via the

criteria
method.
users = UserRepository.criteria(
  :where => {
    :first_name => 'Fred'
  },
  :order => {
    :last_name => :asc
  },
  :limit => [10, 20]
)

Currently implemented criteria options:

  • where(Hash)
  • match(Hash)
  • order(Hash)
  • limit(Array)

Note: it does not currently support searching attributes from different data providers.

Saving/updating entities

Accepts an entity.

There is also

save!
that raises
Datamappify::Data::EntityNotSaved
.
UserRepository.save(user)

Datamappify supports attribute dirty tracking - only dirty attributes will be saved.

Mark attributes as dirty

Sometimes it's useful to manually mark the whole entity, or some attributes in the entity to be dirty. In this case, you could:

UserRepository.states.mark_as_dirty(user) # marks the whole entity as dirty

UserRepository.states.find(user).changed? #=> true UserRepository.states.find(user).first_name_changed? #=> true UserRepository.states.find(user).last_name_changed? #=> true UserRepository.states.find(user).age_changed? #=> true

Or:

UserRepository.states.mark_as_dirty(user, :first_name, :last_name) # marks only first_name and last_name as dirty

UserRepository.states.find(user).changed? #=> true UserRepository.states.find(user).first_name_changed? #=> true UserRepository.states.find(user).last_name_changed? #=> true UserRepository.states.find(user).age_changed? #=> false

Destroying an entity

Accepts an entity.

There is also

destroy!
that raises
Datamappify::Data::EntityNotDestroyed
.

Note that due to the attributes mapping, any data found in mapped records are not touched. For example the corresponding

ActiveRecord::User
record will be destroyed, but
ActiveRecord::Hobby
that is associated will not.
UserRepository.destroy(user)

Initialising an entity

Accepts an entity class and returns a new entity.

This is useful for using

before_init
and
after_init
callbacks to set up the entity.
UserRepository.init(user_class) #=> user

Callbacks

Datamappify supports the following callbacks via Hooks:

  • before_init
  • before_load
  • before_find
  • before_create
  • before_update
  • before_save
  • before_destroy
  • after_init
  • after_load
  • after_find
  • after_create
  • after_update
  • after_save
  • after_destroy

Callbacks are defined in repositories, and they have access to the entity. For example:

class UserRepository
  include Datamappify::Repository

before_create :make_me_admin before_create :make_me_awesome after_save :make_me_smile

private

def make_me_admin(entity) # ... end

def make_me_awesome(entity) # ... end

def make_me_smile(entity) # ... end

...

end

Note: Returning either

nil
or
false
from the callback will cancel all subsequent callbacks (and the action itself, if it's a
before_
callback).

Association

Datamappify also supports entity association. It is experimental and it currently supports the following association types:

  • belongs_to (partially implemented)
  • has_one
  • has_many

Set up your entities and repositories:

# entities

class User include Datamappify::Entity

has_one :title, :via => Title has_many :posts, :via => Post end

class Title include Datamappify::Entity

belongs_to :user end

class Post include Datamappify::Entity

belongs_to :user end

repositories

class UserRepository include Datamappify::Repository

for_entity User

references :title, :via => TitleRepository references :posts, :via => PostRepository end

class TitleRepository include Datamappify::Repository

for_entity Title end

class PostRepository include Datamappify::Repository

for_entity Post end

Usage examples:

new_post         = Post.new(post_attributes)
another_new_post = Post.new(post_attributes)
user             = UserRepository.find(1)
user.title       = Title.new(title_attributes)
user.posts       = [new_post, another_new_post]

persisted_user = UserRepository.save!(user)

persisted_user.title #=> associated title persisted_user.posts #=> an array of associated posts

Nested attributes in forms

Like ActiveRecord and ActionView, Datamappify also supports nested attributes via

fields_for
or
simple_fields_for
.
# slim template

= simple_form_for @post do |f| = f.input :title = f.input :body

= f.simple_fields_for :comment do |fp| = fp.input :author_name = fp.input :comment_body

Default configuration

You may configure Datamappify's default behaviour. In Rails you would put it in an initializer file.

Datamappify.config do |c|
  c.default_provider = :ActiveRecord
end

Built-in extensions

Datamappify ships with a few extensions to make certain tasks easier.

Kaminari

Use

Criteria
with
page
and
per
.
UserRepository.criteria(
  :where => {
    :gender => 'male',
    :age    => 42
  },
  :page => 1,
  :per  => 10
)

API Documentation

More Reading

You may check out this article for more examples.

Changelog

Refer to CHANGELOG.

Todo

  • Performance tuning and query optimisation
  • Authoritative source.
  • Support for configurable primary keys and reference (foreign) keys.

Similar Projects

Credits

License

Licensed under MIT

Bitdeli Badge

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.