README.md in hanami-model-0.0.0 vs README.md in hanami-model-0.6.0
- old
+ new
@@ -1,11 +1,44 @@
# Hanami::Model
-Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/hanami/model`. To experiment with that code, run `bin/console` for an interactive prompt.
+A persistence framework for [Hanami](http://hanamirb.org).
-TODO: Delete this and the text above, and describe your gem
+It delivers a convenient public API to execute queries and commands against a database.
+The architecture eases keeping the business logic (entities) separated from details such as persistence or validations.
+It implements the following concepts:
+
+ * [Entity](#entities) - An object defined by its identity.
+ * [Repository](#repositories) - An object that mediates between the entities and the persistence layer.
+ * [Data Mapper](#data-mapper) - A persistence mapper that keep entities independent from database details.
+ * [Adapter](#adapter) – A database adapter.
+ * [Query](#query) - An object that represents a database query.
+
+Like all the other Hanami components, it can be used as a standalone framework or within a full Hanami application.
+
+## Status
+
+[![Gem Version](https://badge.fury.io/rb/hanami-model.svg)](http://badge.fury.io/rb/hanami-model)
+[![Build Status](https://secure.travis-ci.org/hanami/model.svg?branch=master)](http://travis-ci.org/hanami/model?branch=master)
+[![Coverage](https://img.shields.io/coveralls/hanami/model/master.svg)](https://coveralls.io/r/hanami/model)
+[![Code Climate](https://img.shields.io/codeclimate/github/hanami/model.svg)](https://codeclimate.com/github/hanami/model)
+[![Dependencies](https://gemnasium.com/hanami/model.svg)](https://gemnasium.com/hanami/model)
+[![Inline docs](http://inch-ci.org/github/hanami/model.png)](http://inch-ci.org/github/hanami/model)
+
+## Contact
+
+* Home page: http://hanamirb.org
+* Mailing List: http://hanamirb.org/mailing-list
+* API Doc: http://rdoc.info/gems/hanami-model
+* Bugs/Issues: https://github.com/hanami/model/issues
+* Support: http://stackoverflow.com/questions/tagged/hanami
+* Chat: https://chat.hanamirb.org
+
+## Rubies
+
+__Hanami::Model__ supports Ruby (MRI) 2.2+ and JRuby 9000+
+
## Installation
Add this line to your application's Gemfile:
```ruby
@@ -20,17 +53,577 @@
$ gem install hanami-model
## Usage
-TODO: Write usage instructions here
+This class provides a DSL to configure adapter, mapping and collection.
-## Development
+```ruby
+require 'hanami/model'
-After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
+class User
+ include Hanami::Entity
+ attributes :name, :age
+end
-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`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
+class UserRepository
+ include Hanami::Repository
+end
+Hanami::Model.configure do
+ adapter type: :sql, uri: 'postgres://localhost/database'
+
+ mapping do
+ collection :users do
+ entity User
+ repository UserRepository
+
+ attribute :id, Integer
+ attribute :name, String
+ attribute :age, Integer
+ end
+ end
+end
+
+Hanami::Model.load!
+
+user = User.new(name: 'Luca', age: 32)
+user = UserRepository.create(user)
+
+puts user.id # => 1
+
+u = UserRepository.find(user.id)
+u == user # => true
+```
+
+## Concepts
+
+### Entities
+
+An object that is defined by its identity.
+See "Domain Driven Design" by Eric Evans.
+
+An entity is the core of an application, where the part of the domain logic is implemented.
+It's a small, cohesive object that expresses coherent and meaningful behaviors.
+
+It deals with one and only one responsibility that is pertinent to the
+domain of the application, without caring about details such as persistence
+or validations.
+
+This simplicity of design allows developers to focus on behaviors, or
+message passing if you will, which is the quintessence of Object Oriented Programming.
+
+```ruby
+require 'hanami/model'
+
+class Person
+ include Hanami::Entity
+ attributes :name, :age
+end
+```
+
+When a class includes `Hanami::Entity` it receives the following interface:
+
+ * `#id`
+ * `#id=`
+ * `#initialize(attributes = {})`
+
+`Hanami::Entity` also provides the `.attributes` for defining attribute accessors for the given names.
+
+If we expand the code above in **pure Ruby**, it would be:
+
+```ruby
+class Person
+ attr_accessor :id, :name, :age
+
+ def initialize(attributes = {})
+ @id, @name, @age = attributes.values_at(:id, :name, :age)
+ end
+end
+```
+
+**Hanami::Model** ships `Hanami::Entity` for developers's convenience.
+
+**Hanami::Model** depends on a narrow and well-defined interface for an Entity - `#id`, `#id=`, `#initialize(attributes={})`.
+If your object implements that interface then that object can be used as an Entity in the **Hanami::Model** framework.
+
+However, we suggest to implement this interface by including `Hanami::Entity`, in case that future versions of the framework will expand it.
+
+See [Dependency Inversion Principle](http://en.wikipedia.org/wiki/Dependency_inversion_principle) for more on interfaces.
+
+When a class extends a `Hanami::Entity` class, it will also *inherit* its mother's attributes.
+
+```ruby
+require 'hanami/model'
+
+class Article
+ include Hanami::Entity
+ attributes :name
+end
+
+class RareArticle < Article
+ attributes :price
+end
+```
+
+That is, `RareArticle`'s attributes carry over `:name` attribute from `Article`,
+thus is `:id, :name, :price`.
+
+### Repositories
+
+An object that mediates between entities and the persistence layer.
+It offers a standardized API to query and execute commands on a database.
+
+A repository is **storage independent**, all the queries and commands are
+delegated to the current adapter.
+
+This architecture has several advantages:
+
+ * Applications depend on a standard API, instead of low level details
+ (Dependency Inversion principle)
+
+ * Applications depend on a stable API, that doesn't change if the
+ storage changes
+
+ * Developers can postpone storage decisions
+
+ * Confines persistence logic at a low level
+
+ * Multiple data sources can easily coexist in an application
+
+When a class includes `Hanami::Repository`, it will receive the following interface:
+
+ * `.persist(entity)` – Create or update an entity
+ * `.create(entity)` – Create a record for the given entity
+ * `.update(entity)` – Update the record corresponding to the given entity
+ * `.delete(entity)` – Delete the record corresponding to the given entity
+ * `.all` - Fetch all the entities from the collection
+ * `.find` - Fetch an entity from the collection by its ID
+ * `.first` - Fetch the first entity from the collection
+ * `.last` - Fetch the last entity from the collection
+ * `.clear` - Delete all the records from the collection
+ * `.query` - Fabricates a query object
+
+**A collection is a homogenous set of records.**
+It corresponds to a table for a SQL database or to a MongoDB collection.
+
+**All the queries are private**.
+This decision forces developers to define intention revealing API, instead of leaking storage API details outside of a repository.
+
+Look at the following code:
+
+```ruby
+ArticleRepository.where(author_id: 23).order(:published_at).limit(8)
+```
+
+This is **bad** for a variety of reasons:
+
+ * The caller has an intimate knowledge of the internal mechanisms of the Repository.
+
+ * The caller works on several levels of abstraction.
+
+ * It doesn't express a clear intent, it's just a chain of methods.
+
+ * The caller can't be easily tested in isolation.
+
+ * If we change the storage, we are forced to change the code of the caller(s).
+
+There is a better way:
+
+```ruby
+require 'hanami/model'
+
+class ArticleRepository
+ include Hanami::Repository
+
+ def self.most_recent_by_author(author, limit = 8)
+ query do
+ where(author_id: author.id).
+ order(:published_at)
+ end.limit(limit)
+ end
+end
+```
+
+This is a **huge improvement**, because:
+
+ * The caller doesn't know how the repository fetches the entities.
+
+ * The caller works on a single level of abstraction. It doesn't even know about records, only works with entities.
+
+ * It expresses a clear intent.
+
+ * The caller can be easily tested in isolation. It's just a matter of stubbing this method.
+
+ * If we change the storage, the callers aren't affected.
+
+Here is an extended example of a repository that uses the SQL adapter.
+
+```ruby
+class ArticleRepository
+ include Hanami::Repository
+
+ def self.most_recent_by_author(author, limit = 8)
+ query do
+ where(author_id: author.id).
+ desc(:id).
+ limit(limit)
+ end
+ end
+
+ def self.most_recent_published_by_author(author, limit = 8)
+ most_recent_by_author(author, limit).published
+ end
+
+ def self.published
+ query do
+ where(published: true)
+ end
+ end
+
+ def self.drafts
+ exclude published
+ end
+
+ def self.rank
+ published.desc(:comments_count)
+ end
+
+ def self.best_article_ever
+ rank.limit(1)
+ end
+
+ def self.comments_average
+ query.average(:comments_count)
+ end
+end
+```
+
+You can also extract the common logic from your repository into a module to reuse it in other repositories. Here is a pagination example:
+
+```ruby
+module RepositoryHelpers
+ module Pagination
+ def paginate(limit: 10, offset: 0)
+ query do
+ limit(limit).offset(offset)
+ end
+ end
+ end
+end
+
+class ArticleRepository
+ include Hanami::Repository
+ extend RepositoryHelpers::Pagination
+
+ def self.published
+ query do
+ where(published: true)
+ end
+ end
+
+ # other repository-specific methods here
+end
+```
+
+That will allow `.paginate` usage on `ArticleRepository`, for example:
+`ArticleRepository.published.paginate(15, 0)`
+
+**Your models and repositories have to be in the same namespace.** Otherwise `Hanami::Model::Mapper#load!`
+will not initialize your repositories correctly.
+
+```ruby
+class MyHanamiApp::Model::User
+ include Hanami::Entity
+ # your code here
+end
+
+# This repository will work...
+class MyHanamiApp::Model::UserRepository
+ include Hanami::Repository
+ # your code here
+end
+
+# ...this will not!
+class MyHanamiApp::Repository::UserRepository
+ include Hanami::Repository
+ # your code here
+end
+```
+
+### Data Mapper
+
+A persistence mapper that keeps entities independent from database details.
+It is database independent, it can work with SQL, document, and even with key/value stores.
+
+The role of a data mapper is to translate database columns into the corresponding attribute of an entity.
+
+```ruby
+require 'hanami/model'
+
+mapper = Hanami::Model::Mapper.new do
+ collection :users do
+ entity User
+
+ attribute :id, Integer
+ attribute :name, String
+ attribute :age, Integer
+ end
+end
+```
+
+For simplicity's sake, imagine that the mapper above is used with a SQL database.
+We use `#collection` to indicate the name of the table that we want to map, `#entity` to indicate the class that we want to associate.
+In the end, each call to `#attribute` associates the specified column with a corresponding Ruby type.
+
+For advanced mapping and legacy databases, please have a look at the API doc.
+
+**Known limitations**
+
+Note there are limitations with inherited entities:
+
+```ruby
+require 'hanami/model'
+
+class Article
+ include Hanami::Entity
+ attributes :name
+end
+
+class RareArticle < Article
+ attributes :price
+end
+
+mapper = Hanami::Model::Mapper.new do
+ collection :articles do
+ entity Article
+
+ attribute :id, Integer
+ attribute :name, String
+ attribute :price, Integer
+ end
+end
+```
+
+In the example above, there are a few problems:
+
+* `Article` could not be fetched because mapping could not map `price`.
+* Finding a persisted `RareArticle` record, for eg. `ArticleRepository.find(123)`,
+the result is an `Article` not `RareArticle`.
+
+### Adapter
+
+An adapter is a concrete implementation of persistence logic for a specific database.
+**Hanami::Model** is shipped with three adapters:
+
+ * SqlAdapter
+ * MemoryAdapter
+ * FileSystemAdapter
+
+An adapter can be associated with one or multiple repositories.
+
+```ruby
+require 'pg'
+require 'hanami/model'
+require 'hanami/model/adapters/sql_adapter'
+
+mapper = Hanami::Model::Mapper.new do
+ # ...
+end
+
+adapter = Hanami::Model::Adapters::SqlAdapter.new(mapper, 'postgres://host:port/database')
+
+PersonRepository.adapter = adapter
+ArticleRepository.adapter = adapter
+```
+
+In the example above, we reuse the adapter because the target tables (`people` and `articles`) are defined in the same database.
+**As rule of thumb, one adapter instance per database.**
+
+### Query
+
+An object that implements an interface for querying the database.
+This interface may vary, according to the adapter's specifications.
+
+Here is common interface for existing class:
+
+ * `.all` - Resolves the query by fetching records from the database and translating them into entities
+ * `.where`, `.and` - Adds a condition that behaves like SQL `WHERE`
+ * `.or` - Adds a condition that behaves like SQL `OR`
+ * `.exclude`, `.not` - Logical negation of a #where condition
+ * `.select` - Selects only the specified columns
+ * `.order`, `.asc` - Specify the ascending order of the records, sorted by the given columns
+ * `.reverse_order`, `.desc` - Specify the descending order of the records, sorted by the given columns
+ * `.limit` - Limit the number of records to return
+ * `.offset` - Specify an `OFFSET` clause. Due to SQL syntax restriction, offset MUST be used with `#limit`
+ * `.sum` - Returns the sum of the values for the given column
+ * `.average`, `.avg` - Returns the average of the values for the given column
+ * `.max` - Returns the maximum value for the given column
+ * `.min` - Returns the minimum value for the given column
+ * `.interval` - Returns the difference between the MAX and MIN for the given column
+ * `.range` - Returns a range of values between the MAX and the MIN for the given column
+ * `.exist?` - Checks if at least one record exists for the current conditions
+ * `.count` - Returns a count of the records for the current conditions
+ * `.join` - Adds an inner join with a table (only SQL)
+ * `.left_join` - Adds a left join with a table (only SQL)
+
+If you need more information regarding those methods, you can use comments from [memory](https://github.com/hanami/model/blob/master/lib/hanami/model/adapters/memory/query.rb#L29) or [sql](https://github.com/hanami/model/blob/master/lib/hanami/model/adapters/sql/query.rb#L28) adapters interface.
+
+Think of an adapter for Redis, it will probably employ different strategies to filter records than an SQL query object.
+
+### Conventions
+
+ * A repository must be named after an entity, by appending `"Repository"` to the entity class name (eg. `Article` => `ArticleRepository`).
+
+### Configurations
+
+ * Non-standard repository can be configured for an entity, by setting `repository` on the collection.
+
+ ```ruby
+ require 'hanami/model'
+
+ mapper = Hanami::Model::Mapper.new do
+ collection :users do
+ entity User
+ repository EmployeeRepository
+ end
+ end
+ ```
+
+### Thread safety
+
+**Hanami::Model**'s is thread safe during the runtime, but it isn't during the loading process.
+The mapper compiles some code internally, be sure to safely load it before your application starts.
+
+```ruby
+Mutex.new.synchronize do
+ Hanami::Model.load!
+end
+```
+
+**This is not necessary, when Hanami::Model is used within a Hanami application.**
+
+## Features
+
+### Timestamps
+
+If an entity has the following accessors: `:created_at` and `:updated_at`, they will be automatically updated when the entity is persisted.
+
+```ruby
+require 'hanami/model'
+
+class User
+ include Hanami::Entity
+ attributes :name, :created_at, :updated_at
+end
+
+class UserRepository
+ include Hanami::Repository
+end
+
+Hanami::Model.configure do
+ adapter type: :memory, uri: 'memory://localhost/timestamps'
+
+ mapping do
+ collection :users do
+ entity User
+ repository UserRepository
+
+ attribute :id, Integer
+ attribute :name, String
+ attribute :created_at, DateTime
+ attribute :updated_at, DateTime
+ end
+ end
+end.load!
+
+user = User.new(name: 'L')
+puts user.created_at # => nil
+puts user.updated_at # => nil
+
+user = UserRepository.create(user)
+puts user.created_at.to_s # => "2015-05-15T10:12:20+00:00"
+puts user.updated_at.to_s # => "2015-05-15T10:12:20+00:00"
+
+sleep 3
+user.name = "Luca"
+user = UserRepository.update(user)
+puts user.created_at.to_s # => "2015-05-15T10:12:20+00:00"
+puts user.updated_at.to_s # => "2015-05-15T10:12:23+00:00"
+```
+
+### Dirty Tracking
+
+Entities are able to track changes of their data, if `Hanami::Entity::DirtyTracking` is included.
+
+```ruby
+require 'hanami/model'
+
+class User
+ include Hanami::Entity
+ include Hanami::Entity::DirtyTracking
+ attributes :name, :age
+end
+
+class UserRepository
+ include Hanami::Repository
+end
+
+Hanami::Model.configure do
+ adapter type: :memory, uri: 'memory://localhost/dirty_tracking'
+
+ mapping do
+ collection :users do
+ entity User
+ repository UserRepository
+
+ attribute :id, Integer
+ attribute :name, String
+ attribute :age, String
+ end
+ end
+end.load!
+
+user = User.new(name: 'L')
+user.changed? # => false
+
+user.age = 33
+user.changed? # => true
+user.changed_attributes # => {:age=>33}
+
+user = UserRepository.create(user)
+user.changed? # => false
+
+user.update(name: 'Luca')
+user.changed? # => true
+user.changed_attributes # => {:name=>"Luca"}
+
+user = UserRepository.update(user)
+user.changed? # => false
+
+result = UserRepository.find(user.id)
+result.changed? # => false
+```
+
+## Example
+
+For a full working example, have a look at [EXAMPLE.md](https://github.com/hanami/model/blob/master/EXAMPLE.md).
+Please remember that the setup code is only required for the standalone usage of **Hanami::Model**.
+A **Hanami** application will handle that configurations for you.
+
+## Versioning
+
+__Hanami::Model__ uses [Semantic Versioning 2.0.0](http://semver.org)
+
## Contributing
-Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/hanami-model.
+1. Fork it ( https://github.com/hanami/model/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 new Pull Request
+## Copyright
+
+Copyright © 2014-2016 Luca Guidi – Released under MIT License
+
+This project was formerly known as Lotus (`lotus-model`).