Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

NotificationsYou must be signed in to change notification settings

fredwu/datamappify

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

302 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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 outLotus::Model.


DatamappifyGem VersionBuild StatusCoverage StatusCode 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 theRepository Pattern andEntity Aggregation, and is built on top ofVirtus 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 aUserAccount ActiveRecord model that is attached to aUser 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 callingUserRepository.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 anddestroy, 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 theDatamapper project.

Built-in ORMs for Persistence

You may implement your owndata 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 usesVirtus DSL for defining attributes andActiveModel::Validations DSL for validations.

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

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

classUserincludeDatamappify::Entityattribute:first_name,Stringattribute:last_name,Stringattribute:age,Integerattribute:passport,Stringattribute:driver_license,Stringattribute: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#   endattributes_fromJob# 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#   endattributes_fromHobby,: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.idreferences:accountvalidates:first_name,:presence=>true,:length=>{:minimum=>2}validates:passport,:presence=>true,:length=>{:minimum=>8}deffull_name"#{first_name}#{last_name}"endend

Entity inheritance

Inheritance is supported for entities, for example:

classAdminUser <Userattribute:level,IntegerendclassGuestUser <Userattribute:expiry,DateTimeend

Lazy loading

Datamappify supports attribute lazy loading via theLazy module.

classUserincludeDatamappify::EntityincludeDatamappify::Lazyend

When an entity is lazy loaded, only attributes from the primary source (e.g.User entity's primary source would beActiveRecord::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 todifferent ORMs!

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

classUserRepositoryincludeDatamappify::Repository# specify the entity classfor_entityUser# specify the default data provider for unmapped attributes# optionally you may use `Datamappify.config` to config this globallydefault_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=>:Sequelmap_attribute:health_care,:to=>'UserHealthCare#number',:provider=>:Sequel# alternatively, you may group attribute mappings if they share certain options:group:provider=>:Sequeldomap_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` insteadmap_attribute:bio,:to=>'Bio#body'map_attribute:bio,:to=>'Bio#body',:reference_key=>:person_idend

Repository inheritance

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

classAdminUserRepository <UserRepositoryfor_entityAdminUserendclassGuestUserRepository <UserRepositoryfor_entityGuestUsermap_attribute:expiry,:to=>'User#expiry_date'end

In the above example, both repositories deal with theActiveRecord::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, aDatamppify::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 theDatamappify::Data::Record::NameOfDataProvider namespace:

moduleUserDataclassDriverLicense <ActiveRecord::Base# your customisation...endend
moduleDatamappify::Data::Record::ActiveRecord::UserDataclassDriverLicense < ::ActiveRecord::Base# your customisation...endend

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 thecriteria 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<limit(Integer), offset(Integer)>)

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

Saving/updating entities

Accepts an entity.

There is alsosave! that raisesDatamappify::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 dirtyUserRepository.states.find(user).changed?#=> trueUserRepository.states.find(user).first_name_changed?#=> trueUserRepository.states.find(user).last_name_changed?#=> trueUserRepository.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 dirtyUserRepository.states.find(user).changed?#=> trueUserRepository.states.find(user).first_name_changed?#=> trueUserRepository.states.find(user).last_name_changed?#=> trueUserRepository.states.find(user).age_changed?#=> false

Destroying an entity

Accepts an entity.

There is alsodestroy! that raisesDatamappify::Data::EntityNotDestroyed.

Note that due to the attributes mapping, any data found in mapped records are not touched. For example the correspondingActiveRecord::User record will be destroyed, butActiveRecord::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 usingbefore_init andafter_init callbacks to set up the entity.

UserRepository.init(user_class)#=> user

Callbacks

Datamappify supports the following callbacks viaHooks:

  • 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:

classUserRepositoryincludeDatamappify::Repositorybefore_create:make_me_adminbefore_create:make_me_awesomeafter_save:make_me_smileprivatedefmake_me_admin(entity)# ...enddefmake_me_awesome(entity)# ...enddefmake_me_smile(entity)# ...end# ...end

Note: Returning eithernil orfalse from the callback will cancel all subsequent callbacks (and the action itself, if it's abefore_ 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:

# entitiesclassUserincludeDatamappify::Entityhas_one:title,:via=>Titlehas_many:posts,:via=>PostendclassTitleincludeDatamappify::Entitybelongs_to:userendclassPostincludeDatamappify::Entitybelongs_to:userend# repositoriesclassUserRepositoryincludeDatamappify::Repositoryfor_entityUserreferences:title,:via=>TitleRepositoryreferences:posts,:via=>PostRepositoryendclassTitleRepositoryincludeDatamappify::Repositoryfor_entityTitleendclassPostRepositoryincludeDatamappify::Repositoryfor_entityPostend

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 titlepersisted_user.posts#=> an array of associated posts

Nested attributes in forms

Like ActiveRecord and ActionView, Datamappify also supports nested attributes viafields_for orsimple_fields_for.

# slim template=simple_form_for@postdo |f|=f.input:title=f.input:body=f.simple_fields_for:commentdo |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.configdo |c|c.default_provider=:ActiveRecordend

Built-in extensions

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

Kaminari

UseCriteria withpage andper.

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

API Documentation

More Reading

You may check out thisarticle for more examples.

Changelog

Refer toCHANGELOG.

Todo

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

Similar Projects

Credits

License

Licensed underMIT

Bitdeli Badge

About

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

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors6

Languages


[8]ページ先頭

©2009-2026 Movatter.jp