{"id":13879915,"url":"https://github.com/fredwu/datamappify","last_synced_at":"2025-04-04T10:09:02.346Z","repository":{"id":62556760,"uuid":"790660","full_name":"fredwu/datamappify","owner":"fredwu","description":"Compose, decouple and manage domain logic and data persistence separately. Works particularly great for composing form objects!","archived":false,"fork":false,"pushed_at":"2014-08-05T13:25:31.000Z","size":1516,"stargazers_count":332,"open_issues_count":3,"forks_count":18,"subscribers_count":24,"default_branch":"master","last_synced_at":"2025-03-28T09:08:58.493Z","etag":null,"topics":["activerecord","data-mapper","data-mapping","data-persistence","entity","orm","repository-pattern"],"latest_commit_sha":null,"homepage":"http://fredwu.me/","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fredwu.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2010-07-22T11:18:40.000Z","updated_at":"2024-12-09T04:12:47.000Z","dependencies_parsed_at":"2022-11-03T06:00:52.678Z","dependency_job_id":null,"html_url":"https://github.com/fredwu/datamappify","commit_stats":null,"previous_names":[],"tags_count":21,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fredwu%2Fdatamappify","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fredwu%2Fdatamappify/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fredwu%2Fdatamappify/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fredwu%2Fdatamappify/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fredwu","download_url":"https://codeload.github.com/fredwu/datamappify/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247157283,"owners_count":20893220,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["activerecord","data-mapper","data-mapping","data-persistence","entity","orm","repository-pattern"],"created_at":"2024-08-06T08:02:38.897Z","updated_at":"2025-04-04T10:09:02.319Z","avatar_url":"https://github.com/fredwu.png","language":"Ruby","readme":"__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.__\n\nFeel free to read the README and browse the code, I still believe in the solutions for this particular domain.\n\nFor a more active albeit still young project, check out [Lotus::Model](https://github.com/lotus/model).\n\n---\n\n# Datamappify [![Gem Version](https://badge.fury.io/rb/datamappify.png)](http://badge.fury.io/rb/datamappify) [![Build Status](https://api.travis-ci.org/fredwu/datamappify.png?branch=master)](http://travis-ci.org/fredwu/datamappify) [![Coverage Status](https://coveralls.io/repos/fredwu/datamappify/badge.png)](https://coveralls.io/r/fredwu/datamappify) [![Code Climate](https://codeclimate.com/github/fredwu/datamappify.png)](https://codeclimate.com/github/fredwu/datamappify)\n\n#### Compose, decouple and manage domain logic and data persistence separately. Works particularly great for composing form objects!\n\n## Overview\n\nThe 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.\n\nDatamappify tries to solve two common problems in web applications:\n\n1. The coupling between domain logic and data persistence.\n2. The coupling between forms and models.\n\nDatamappify is loosely based on the [Repository Pattern](http://martinfowler.com/eaaCatalog/repository.html) and [Entity Aggregation](http://msdn.microsoft.com/en-au/library/ff649505.aspx), and is built on top of [Virtus](https://github.com/solnic/virtus) and existing ORMs (ActiveRecord and Sequel, etc).\n\nThere are three main design goals:\n\n1. To utilise the powerfulness of existing ORMs so that using Datamappify doesn't interrupt too much of your current workflow. For example, [Devise](https://github.com/plataformatec/devise) would still work if you use it with a `UserAccount` ActiveRecord model that is attached to a `User` entity managed by Datamappify.\n2. To have a flexible entity model that works great with dealing with form data. For example, [SimpleForm](https://github.com/plataformatec/simple_form) would still work with nested attributes from different ORM models if you map entity attributes smartly in your repositories managed by Datamappify.\n3. 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!\n\nDatamappify consists of three components:\n\n- __Entity__ contains models behaviour, think an ActiveRecord model with the persistence specifics removed.\n- __Repository__ is responsible for data retrieval and persistence, e.g. `find`, `save` and `destroy`, etc.\n- __Data__ as the name suggests, holds your model data. It contains ORM objects (e.g. ActiveRecord models).\n\nBelow is a high level and somewhat simplified overview of Datamappify's architecture.\n\n![](http://i.imgur.com/BvtEO1u.png)\n\nNote: Datamappify is NOT affiliated with the [Datamapper](https://github.com/datamapper/) project.\n\n### Built-in ORMs for Persistence\n\nYou may implement your own [data provider and criteria](lib/datamappify/data), but Datamappify comes with build-in support for the following ORMS:\n\n- ActiveRecord\n- Sequel\n\n## Requirements\n\n- ruby 2.0+\n- ActiveModel 4.0+\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n    gem 'datamappify'\n\n## Usage\n\n### Entity\n\nEntity uses [Virtus](https://github.com/solnic/virtus) DSL for defining attributes and [ActiveModel::Validations](http://api.rubyonrails.org/classes/ActiveModel/Validations.html) DSL for validations.\n\nThe cool thing about Virtus is that all your attributes get [coercion](https://github.com/solnic/virtus#collection-member-coercions) for free!\n\nBelow is an example of a User entity, with inline comments on how some of the DSLs work.\n\n```ruby\nclass User\n  include Datamappify::Entity\n\n  attribute :first_name,     String\n  attribute :last_name,      String\n  attribute :age,            Integer\n  attribute :passport,       String\n  attribute :driver_license, String\n  attribute :health_care,    String\n\n  # Nested entity composition - composing the entity with attributes and validations from other entities\n  #\n  #   class Job\n  #     include Datamappify::Entity\n  #\n  #     attributes :title, String\n  #     validates  :title, :presence =\u003e true\n  #   end\n  #\n  #   class User\n  #     # ...\n  #     attributes_from Job\n  #   end\n  #\n  # essentially equals:\n  #\n  #   class User\n  #     # ...\n  #     attributes :title, String\n  #     validates  :title, :presence =\u003e true\n  #   end\n  attributes_from Job\n\n  # optionally you may prefix the attributes, so that:\n  #\n  #   class Hobby\n  #     include Datamappify::Entity\n  #\n  #     attributes :name, String\n  #     validates  :name, :presence =\u003e true\n  #   end\n  #\n  #   class User\n  #     # ...\n  #     attributes_from Hobby, :prefix_with =\u003e :hobby\n  #   end\n  #\n  # becomes:\n  #\n  #   class User\n  #     # ...\n  #     attributes :hobby_name, String\n  #     validates  :hobby_name, :presence =\u003e true\n  #   end\n  attributes_from Hobby, :prefix_with =\u003e :hobby\n\n  # Entity reference\n  #\n  # `references` is a convenient method for:\n  #\n  #   attribute :account_id, Integer\n  #   attr_accessor :account\n  #\n  # and it assigns `account_id` the correct value:\n  #\n  #   user.account = account #=\u003e user.account_id = account.id\n  references :account\n\n  validates :first_name, :presence =\u003e true,\n                         :length   =\u003e { :minimum =\u003e 2 }\n  validates :passport,   :presence =\u003e true,\n                         :length   =\u003e { :minimum =\u003e 8 }\n\n  def full_name\n    \"#{first_name} #{last_name}\"\n  end\nend\n```\n\n#### Entity inheritance\n\nInheritance is supported for entities, for example:\n\n```ruby\nclass AdminUser \u003c User\n  attribute :level, Integer\nend\n\nclass GuestUser \u003c User\n  attribute :expiry, DateTime\nend\n```\n\n#### Lazy loading\n\nDatamappify supports attribute lazy loading via the `Lazy` module.\n\n```ruby\nclass User\n  include Datamappify::Entity\n  include Datamappify::Lazy\nend\n```\n\nWhen 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.\n\n### Repository\n\nRepository maps entity attributes to DB columns - better yet, you can even map attributes to __different ORMs__!\n\nBelow is an example of a repository for the User entity, you can have more than one repositories for the same entity.\n\n```ruby\nclass UserRepository\n  include Datamappify::Repository\n\n  # specify the entity class\n  for_entity User\n\n  # specify the default data provider for unmapped attributes\n  # optionally you may use `Datamappify.config` to config this globally\n  default_provider :ActiveRecord\n\n  # specify any attributes that need to be mapped\n  #\n  # for attributes mapped from a different source class, a foreign key on the source class is required\n  #\n  # for example:\n  #   - 'last_name' is mapped to the 'User' ActiveRecord class and its 'surname' attribute\n  #   - 'driver_license' is mapped to the 'UserDriverLicense' ActiveRecord class and its 'number' attribute\n  #   - 'passport' is mapped to the 'UserPassport' Sequel class and its 'number' attribute\n  #   - attributes not specified here are mapped automatically to 'User' with provider 'ActiveRecord'\n  map_attribute :last_name,      :to =\u003e 'User#surname'\n  map_attribute :driver_license, :to =\u003e 'UserDriverLicense#number'\n  map_attribute :passport,       :to =\u003e 'UserPassport#number',   :provider =\u003e :Sequel\n  map_attribute :health_care,    :to =\u003e 'UserHealthCare#number', :provider =\u003e :Sequel\n\n  # alternatively, you may group attribute mappings if they share certain options:\n  group :provider =\u003e :Sequel do\n    map_attribute :passport,    :to =\u003e 'UserPassport#number'\n    map_attribute :health_care, :to =\u003e 'UserHealthCare#number'\n  end\n\n  # attributes can also be reverse mapped by specifying the `via` option\n  #\n  # for example, the below attribute will look for `hobby_id` on the user object,\n  # and map `hobby_name` from the `name` attribute of `ActiveRecord::Hobby`\n  #\n  # this is useful for mapping form fields (similar to ActiveRecord's nested attributes)\n  map_attribute :hobby_name, :to =\u003e 'Hobby#name', :via =\u003e :hobby_id\n\n  # by default, Datamappify maps attributes using an inferred reference (foreign) key,\n  # for example, the first mapping below will look for the `user_id` key in `Bio`,\n  # the second mapping below will look for the `person_id` key in `Bio` instead\n  map_attribute :bio, :to =\u003e 'Bio#body'\n  map_attribute :bio, :to =\u003e 'Bio#body', :reference_key =\u003e :person_id\nend\n```\n\n#### Repository inheritance\n\nInheritance is supported for repositories when your data structure is based on STI ([Single Table Inheritance](http://en.wikipedia.org/wiki/Single_Table_Inheritance)), for example:\n\n```ruby\nclass AdminUserRepository \u003c UserRepository\n  for_entity AdminUser\nend\n\nclass GuestUserRepository \u003c UserRepository\n  for_entity GuestUser\n\n  map_attribute :expiry, :to =\u003e 'User#expiry_date'\nend\n```\n\nIn the above example, both repositories deal with the `ActiveRecord::User` data model.\n\n#### Override mapped data models\n\nDatamappify repository by default creates the underlying data model classes for you. For example:\n\n```ruby\nmap_attribute :driver_license, :to =\u003e 'UserData::DriverLicense#number'\n```\n\nIn 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:\n\n```ruby\nmodule UserData\n  class DriverLicense \u003c ActiveRecord::Base\n    # your customisation...\n  end\nend\n```\n\n```ruby\nmodule Datamappify::Data::Record::ActiveRecord::UserData\n  class DriverLicense \u003c ::ActiveRecord::Base\n    # your customisation...\n  end\nend\n```\n\n### Repository APIs\n\n_More repository APIs are being added, below is a list of the currently implemented APIs._\n\n#### Retrieving an entity\n\nAccepts an id.\n\n```ruby\nuser = UserRepository.find(1)\n```\n\n#### Checking if an entity exists in the repository\n\nAccepts an entity.\n\n```ruby\nUserRepository.exists?(user)\n```\n\n#### Retrieving all entities\n\nReturns an array of entities.\n\n```ruby\nusers = UserRepository.all\n```\n\n#### Searching entities\n\nReturns an array of entities.\n\n##### Simple\n\n```ruby\nusers = UserRepository.where(:first_name =\u003e 'Fred', :driver_license =\u003e 'AABBCCDD')\n```\n\n##### Match\n\n```ruby\nusers = UserRepository.match(:first_name =\u003e 'Fre%', :driver_license =\u003e '%bbcc%')\n```\n\n##### Advanced\n\nYou may compose search criteria via the `criteria` method.\n\n```ruby\nusers = UserRepository.criteria(\n  :where =\u003e {\n    :first_name =\u003e 'Fred'\n  },\n  :order =\u003e {\n    :last_name =\u003e :asc\n  },\n  :limit =\u003e [10, 20]\n)\n```\n\nCurrently implemented criteria options:\n\n- where(Hash)\n- match(Hash)\n- order(Hash)\n- limit(Array\u003climit(Integer), offset(Integer)\u003e)\n\n_Note: it does not currently support searching attributes from different data providers._\n\n#### Saving/updating entities\n\nAccepts an entity.\n\nThere is also `save!` that raises `Datamappify::Data::EntityNotSaved`.\n\n```ruby\nUserRepository.save(user)\n```\n\nDatamappify supports attribute dirty tracking - only dirty attributes will be saved.\n\n##### Mark attributes as dirty\n\nSometimes it's useful to manually mark the whole entity, or some attributes in the entity to be dirty. In this case, you could:\n\n```ruby\nUserRepository.states.mark_as_dirty(user) # marks the whole entity as dirty\n\nUserRepository.states.find(user).changed?            #=\u003e true\nUserRepository.states.find(user).first_name_changed? #=\u003e true\nUserRepository.states.find(user).last_name_changed?  #=\u003e true\nUserRepository.states.find(user).age_changed?        #=\u003e true\n```\n\nOr:\n\n```ruby\nUserRepository.states.mark_as_dirty(user, :first_name, :last_name) # marks only first_name and last_name as dirty\n\nUserRepository.states.find(user).changed?            #=\u003e true\nUserRepository.states.find(user).first_name_changed? #=\u003e true\nUserRepository.states.find(user).last_name_changed?  #=\u003e true\nUserRepository.states.find(user).age_changed?        #=\u003e false\n```\n\n#### Destroying an entity\n\nAccepts an entity.\n\nThere is also `destroy!` that raises `Datamappify::Data::EntityNotDestroyed`.\n\nNote 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.\n\n```ruby\nUserRepository.destroy(user)\n```\n\n#### Initialising an entity\n\nAccepts an entity class and returns a new entity.\n\nThis is useful for using `before_init` and `after_init` callbacks to set up the entity.\n\n```ruby\nUserRepository.init(user_class) #=\u003e user\n```\n\n#### Callbacks\n\nDatamappify supports the following callbacks via [Hooks](https://github.com/apotonick/hooks):\n\n- before_init\n- before_load\n- before_find\n- before_create\n- before_update\n- before_save\n- before_destroy\n- after_init\n- after_load\n- after_find\n- after_create\n- after_update\n- after_save\n- after_destroy\n\nCallbacks are defined in repositories, and they have access to the entity. For example:\n\n```ruby\nclass UserRepository\n  include Datamappify::Repository\n\n  before_create :make_me_admin\n  before_create :make_me_awesome\n  after_save    :make_me_smile\n\n  private\n\n  def make_me_admin(entity)\n    # ...\n  end\n\n  def make_me_awesome(entity)\n    # ...\n  end\n\n  def make_me_smile(entity)\n    # ...\n  end\n\n  # ...\nend\n```\n\nNote: Returning either `nil` or `false` from the callback will cancel all subsequent callbacks (and the action itself, if it's a `before_` callback).\n\n### Association\n\nDatamappify also supports entity association. It is experimental and it currently supports the following association types:\n\n- belongs_to (partially implemented)\n- has_one\n- has_many\n\nSet up your entities and repositories:\n\n```ruby\n# entities\n\nclass User\n  include Datamappify::Entity\n\n  has_one  :title, :via =\u003e Title\n  has_many :posts, :via =\u003e Post\nend\n\nclass Title\n  include Datamappify::Entity\n\n  belongs_to :user\nend\n\nclass Post\n  include Datamappify::Entity\n\n  belongs_to :user\nend\n\n# repositories\n\nclass UserRepository\n  include Datamappify::Repository\n\n  for_entity User\n\n  references :title, :via =\u003e TitleRepository\n  references :posts, :via =\u003e PostRepository\nend\n\nclass TitleRepository\n  include Datamappify::Repository\n\n  for_entity Title\nend\n\nclass PostRepository\n  include Datamappify::Repository\n\n  for_entity Post\nend\n```\n\nUsage examples:\n\n```ruby\nnew_post         = Post.new(post_attributes)\nanother_new_post = Post.new(post_attributes)\nuser             = UserRepository.find(1)\nuser.title       = Title.new(title_attributes)\nuser.posts       = [new_post, another_new_post]\n\npersisted_user   = UserRepository.save!(user)\n\npersisted_user.title #=\u003e associated title\npersisted_user.posts #=\u003e an array of associated posts\n```\n\n### Nested attributes in forms\n\nLike ActiveRecord and ActionView, Datamappify also supports nested attributes via `fields_for` or `simple_fields_for`.\n\n```ruby\n# slim template\n\n= simple_form_for @post do |f|\n  = f.input :title\n  = f.input :body\n\n  = f.simple_fields_for :comment do |fp|\n    = fp.input :author_name\n    = fp.input :comment_body\n```\n\n### Default configuration\n\nYou may configure Datamappify's default behaviour. In Rails you would put it in an initializer file.\n\n```ruby\nDatamappify.config do |c|\n  c.default_provider = :ActiveRecord\nend\n```\n\n### Built-in extensions\n\nDatamappify ships with a few extensions to make certain tasks easier.\n\n#### Kaminari\n\nUse `Criteria` with `page` and `per`.\n\n```ruby\nUserRepository.criteria(\n  :where =\u003e {\n    :gender =\u003e 'male',\n    :age    =\u003e 42\n  },\n  :page =\u003e 1,\n  :per  =\u003e 10\n)\n```\n\n## API Documentation\n\n- [Rubygem release version](http://rubydoc.info/gems/datamappify/frames)\n- [Github master version](http://rubydoc.info/github/fredwu/datamappify/master/frames)\n\n## More Reading\n\nYou may check out this [article](http://fredwu.me/post/54009567748/) for more examples.\n\n## Changelog\n\nRefer to [CHANGELOG](CHANGELOG.md).\n\n## Todo\n\n- Performance tuning and query optimisation\n- [Authoritative source](http://msdn.microsoft.com/en-au/library/ff649505.aspx).\n- Support for configurable primary keys and reference (foreign) keys.\n\n## Similar Projects\n\n- [Curator](https://github.com/braintree/curator)\n- [Edr](https://github.com/nulogy/edr)\n- [Minimapper](https://github.com/joakimk/minimapper)\n- [Reform](https://github.com/apotonick/reform)\n\n## Credits\n\n- [Fred Wu](http://fredwu.me/) - author.\n- [James Ladd](http://jamesladdcode.com/) for reviewing the code and giving advice on architectural decisions.\n- [Locomote](http://www.locomote.com.au/) - where Datamappify was built.\n- And with these [awesome contributors](https://github.com/fredwu/datamappify/contributors)!\n\n## License\n\nLicensed under [MIT](http://fredwu.mit-license.org/)\n\n\n[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/fredwu/datamappify/trend.png)](https://bitdeli.com/free \"Bitdeli Badge\")\n\n","funding_links":[],"categories":["Ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffredwu%2Fdatamappify","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffredwu%2Fdatamappify","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffredwu%2Fdatamappify/lists"}