hanami-model 0.0.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +145 -0
  3. data/EXAMPLE.md +212 -0
  4. data/LICENSE.md +22 -0
  5. data/README.md +600 -7
  6. data/hanami-model.gemspec +17 -12
  7. data/lib/hanami-model.rb +1 -0
  8. data/lib/hanami/entity.rb +298 -0
  9. data/lib/hanami/entity/dirty_tracking.rb +74 -0
  10. data/lib/hanami/model.rb +204 -2
  11. data/lib/hanami/model/adapters/abstract.rb +281 -0
  12. data/lib/hanami/model/adapters/file_system_adapter.rb +288 -0
  13. data/lib/hanami/model/adapters/implementation.rb +111 -0
  14. data/lib/hanami/model/adapters/memory/collection.rb +132 -0
  15. data/lib/hanami/model/adapters/memory/command.rb +113 -0
  16. data/lib/hanami/model/adapters/memory/query.rb +653 -0
  17. data/lib/hanami/model/adapters/memory_adapter.rb +179 -0
  18. data/lib/hanami/model/adapters/null_adapter.rb +24 -0
  19. data/lib/hanami/model/adapters/sql/collection.rb +287 -0
  20. data/lib/hanami/model/adapters/sql/command.rb +73 -0
  21. data/lib/hanami/model/adapters/sql/console.rb +33 -0
  22. data/lib/hanami/model/adapters/sql/consoles/mysql.rb +49 -0
  23. data/lib/hanami/model/adapters/sql/consoles/postgresql.rb +48 -0
  24. data/lib/hanami/model/adapters/sql/consoles/sqlite.rb +26 -0
  25. data/lib/hanami/model/adapters/sql/query.rb +788 -0
  26. data/lib/hanami/model/adapters/sql_adapter.rb +296 -0
  27. data/lib/hanami/model/coercer.rb +74 -0
  28. data/lib/hanami/model/config/adapter.rb +116 -0
  29. data/lib/hanami/model/config/mapper.rb +45 -0
  30. data/lib/hanami/model/configuration.rb +275 -0
  31. data/lib/hanami/model/error.rb +7 -0
  32. data/lib/hanami/model/mapper.rb +124 -0
  33. data/lib/hanami/model/mapping.rb +48 -0
  34. data/lib/hanami/model/mapping/attribute.rb +85 -0
  35. data/lib/hanami/model/mapping/coercers.rb +314 -0
  36. data/lib/hanami/model/mapping/collection.rb +490 -0
  37. data/lib/hanami/model/mapping/collection_coercer.rb +79 -0
  38. data/lib/hanami/model/migrator.rb +324 -0
  39. data/lib/hanami/model/migrator/adapter.rb +170 -0
  40. data/lib/hanami/model/migrator/connection.rb +133 -0
  41. data/lib/hanami/model/migrator/mysql_adapter.rb +72 -0
  42. data/lib/hanami/model/migrator/postgres_adapter.rb +119 -0
  43. data/lib/hanami/model/migrator/sqlite_adapter.rb +110 -0
  44. data/lib/hanami/model/version.rb +4 -1
  45. data/lib/hanami/repository.rb +872 -0
  46. metadata +100 -16
  47. data/.gitignore +0 -9
  48. data/Gemfile +0 -4
  49. data/Rakefile +0 -2
  50. data/bin/console +0 -14
  51. data/bin/setup +0 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d93e87761cb8a7aac28af6df5cdfac35c112c635
4
- data.tar.gz: f0b1f122133ba1fb283ab126da3784d81dafa8a1
3
+ metadata.gz: a99014d711cf7b1e4fd9469acdee5a7fefb69d9f
4
+ data.tar.gz: 71781d9d98ea8e5b5e75bd0df4527ed47f6e6b3a
5
5
  SHA512:
6
- metadata.gz: 4e3f70305ca019849d22f0bd888396b51d0d154c468710a78d98800b9bcc9e1446771c84efcff51d7e56e4e10342e06d9d016e8dac55b368409faabfbaea382c
7
- data.tar.gz: 23dbad87035fe552c51155609bc1ac789c8396d8d01b3931dc9f098125c8a10ac027829ea84ea83663636ceb6b6c46327b09e16c6137943bf3551536196011db
6
+ metadata.gz: 312c76418b3d0e8a8c7c41125c009f03bfdd03c4102de60ed87806df705c9584922ff1afc952b6ae383a86805b209333c1ba31dbfb4706810028bcba598e88f9
7
+ data.tar.gz: 5bba6c44f8cebef794b53c7b7a94d897f09098440feec568f293602fc46e0593c52ce090451cdcaee76e261fc0a908d731aa82ab552afb172b078f07e3be46ba
@@ -0,0 +1,145 @@
1
+ # Hanami::Model
2
+ A persistence layer for Hanami
3
+
4
+ ## v0.6.0 - 2016-01-22
5
+ ### Changed
6
+ - [Luca Guidi] Renamed the project
7
+
8
+ ## v0.5.2 - 2016-01-19
9
+ ### Changed
10
+ - [Sean Collins] Improved error message for `Lotus::Model::Adapters::NoAdapterError`
11
+
12
+ ### Fixed
13
+ - [Kyle Chong & Trung Lê] Catch Sequel exceptions and re-raise as `Lotus::Model::Error`
14
+
15
+ ## v0.5.1 - 2016-01-12
16
+ ### Added
17
+ - [Taylor Finnell] Let `Lotus::Model::Configuration#adapter` to accept arbitrary options (eg. `adapter type: :sql, uri: 'jdbc:...', after_connect: Proc.new { |connection| connection.auto_commit(true) }`)
18
+
19
+ ### Changed
20
+ - [Andrey Deryabin] Improved `Entity#inspect`
21
+ - [Karim Tarek] Introduced `Lotus::Model::Error` and let all the framework exceptions to inherit from it.
22
+
23
+ ### Fixed
24
+ - [Luca Guidi] Improved error message when trying to use a repository without mapping the corresponding collections
25
+ - [Sean Collins] Improved error message when trying to create database, but it fails (eg. missing `createdb` executable)
26
+ - [Andrey Deryabin] Improved error message when trying to drop database, but a client is still connected (useful for PostgreSQL)
27
+ - [Hiếu Nguyễn] Improved error message when trying to "prepare" database, but it fails
28
+
29
+ ## v0.5.0 - 2015-09-30
30
+ ### Added
31
+ - [Brenno Costa] Official support for JRuby 9k+
32
+ - [Luca Guidi] Command/Query separation via `Repository.execute` and `Repository.fetch`
33
+ - [Luca Guidi] Custom attribute coercers for data mapper
34
+ - [Alfonso Uceda] Added `#join` and `#left_join` and `#group` to SQL adapter
35
+
36
+ ### Changed
37
+ - [Luca Guidi] `Repository.execute` no longer returns a result from the database.
38
+
39
+ ### Fixed
40
+ - [Manuel Corrales] Use `dropdb` to drop PostgreSQL database.
41
+ - [Luca Guidi & Bohdan V.] Ignore dotfiles while running migrations.
42
+
43
+ ## v0.4.1 - 2015-07-10
44
+ ### Fixed
45
+ - [Nick Coyne] Fixed database creation for PostgreSQL (now it uses `createdb`).
46
+
47
+ ## v0.4.0 - 2015-06-23
48
+ ### Added
49
+ - [Luca Guidi] Database migrations
50
+
51
+ ### Changed
52
+ - [Matthew Bellantoni] Made `Repository.execute` not callable from the outside (private Ruby method, public API).
53
+
54
+ ## v0.3.2 - 2015-05-22
55
+ ### Added
56
+ - [Dmitry Tymchuk & Luca Guidi] Fix for dirty tracking of attributes changed in place (eg. `book.tags << 'non-fiction'`)
57
+
58
+ ## v0.3.1 - 2015-05-15
59
+ ### Added
60
+ - [Dmitry Tymchuk] Dirty tracking for entities (via `Lotus::Entity::DirtyTracking` module to include)
61
+ - [My Mai] Automatic update of timestamps when an entity is persisted.
62
+ - [Peter Berkenbosch] Introduced `Lotus::Repository#execute`, to execute raw query/commands against database (eg. `BookRepository.execute "SELECT * FROM users"` or `BookRepository.execute "UPDATE users SET admin = 'f'"`)
63
+ - [Guilherme Franco] Memory and File System adapters now accept a block for `where`, `or`, `and` conditions (eg `where { age > 33 }`).
64
+
65
+ ### Fixed
66
+ - [Luca Guidi] Ensure Array coercion to preserve original data structure
67
+
68
+ ## v0.3.0 - 2015-03-23
69
+ ### Added
70
+ - [Linus Pettersson] Database console
71
+
72
+ ### Fixed
73
+ - [Alfonso Uceda Pompa] Don't send unwanted null values to the database, while coercing entities
74
+ - [Jan Lelis] Do not define top-level `Boolean`, because it is already defined by `hanami-utils`
75
+ - [Vsevolod Romashov] Fix entity class resolving in `Coercer#from_record`
76
+ - [Jason Harrelson] Add file and line to `instance_eval` in `Coercer` to make backtrace more usable
77
+
78
+ ## v0.2.4 - 2015-02-20
79
+ ### Fixed
80
+ - [Luca Guidi] When duplicate the framework don't copy over the original `Lotus::Model` configuration
81
+
82
+ ## v0.2.3 - 2015-02-13
83
+ ### Added
84
+ - [Alfonso Uceda Pompa] Added support for database transactions in repositories
85
+
86
+ ### Fixed
87
+ - [Luca Guidi] Ensure file system adapter old data is read when a new process is started
88
+
89
+ ## v0.2.2 - 2015-01-18
90
+ ### Added
91
+ - [Luca Guidi] Coerce entities when persisted
92
+
93
+ ## v0.2.1 - 2015-01-12
94
+ ### Added
95
+ - [Luca Guidi] Compatibility between Lotus::Entity and Lotus::Validations
96
+
97
+ ## v0.2.0 - 2014-12-23
98
+ ### Added
99
+ - [Luca Guidi] Introduced file system adapter
100
+ – [Benny Klotz & Trung Lê] Introduced `Entity` inheritance of attributes
101
+ - [Trung Lê] Introduced `Entity#update` for bulk update of attributes
102
+ - [Luca Guidi] Improved error when try to use a repository which wasn't configured or when the framework wasn't loaded yet
103
+ - [Trung Lê] Introduced `Entity#to_h`
104
+ - [Trung Lê] Introduced `Lotus::Model.duplicate`
105
+ - [Trung Lê] Made `Lotus::Mapper` lazy
106
+ - [Trung Lê] Introduced thread safe autoloading for adapters
107
+ - [Felipe Sere] Add support for `Symbol` coercion
108
+ - [Celso Fernandes] Add support for `BigDecimal` coercion
109
+ - [Trung Lê] Introduced `Lotus::Model.load!` as entry point for loading
110
+ - [Trung Lê] Introduced `Mapper#repository` as DSL to associate a repository to a collection
111
+ - [Trung Lê & Tao Guo] Introduced `Configuration#mapping` as DSL to configure the mapping
112
+ - [Coen Wessels] Allow `where`, `exclude` and `or` to accept blocks
113
+ - [Trung Lê & Tao Guo] Introduced `Configuration#adapter` as DSL to configure the adapter
114
+ - [Trung Lê] Introduced `Lotus::Model::Configuration`
115
+
116
+ ### Changed
117
+ - [Trung Lê] Changed `Entity.attributes=` to `Entity.attributes`
118
+ - [Trung Lê] In case of missing entity, let `Repository#find` returns `nil` instead of raise an exception
119
+
120
+ ### Fixed
121
+ - [Rik Tonnard] Ensure correct behavior of `#offset` in memory adapter
122
+ - [Benny Klotz] Ensure `Entity` to set the attributes even when the given Hash uses strings as keys
123
+ - [Ben Askins] Always return the entity from `Repository#persist`
124
+ - [Jeremy Stephens] Made `Memory::Query#where` and `#or` behave more like the SQL counter-part
125
+
126
+ ## v0.1.2 - 2014-06-26
127
+ ### Fixed
128
+ - [Stanislav Spiridonov] Ensure to require `'hanami/model/mapping/coercions'`
129
+ - [Krzysztof Zalewski] `Entity` defines `#id` accessor by default
130
+
131
+
132
+ ## v0.1.1 - 2014-06-23
133
+ ### Added
134
+ - [Luca Guidi] Introduced `Lotus::Model::Mapping::Coercions` in order to decouple from `Lotus::Utils::Kernel`
135
+ - [Luca Guidi] Official support for Ruby 2.1
136
+
137
+ ## v0.1.0 - 2014-04-23
138
+ ### Added
139
+ - [Luca Guidi] Allow to inject coercer into mapper
140
+ - [Luca Guidi] Introduced database mapping
141
+ - [Luca Guidi] Introduced `Lotus::Entity`
142
+ - [Luca Guidi] Introduced SQL adapter
143
+ - [Luca Guidi] Introduced memory adapter
144
+ – [Luca Guidi] Introduced adapters for repositories
145
+ - [Luca Guidi] Introduced `Lotus::Repository`
@@ -0,0 +1,212 @@
1
+ # Hanami::Model
2
+
3
+ This is a guide that helps you to get started with [**Hanami::Model**](https://github.com/hanami/model).
4
+ You can find the full code source [here](https://gist.github.com/jodosha/11211048).
5
+
6
+ ## Gems
7
+
8
+ First of all, we need to setup a `Gemfile`.
9
+
10
+ ```ruby
11
+ source 'https://rubygems.org'
12
+
13
+ gem 'sqlite3'
14
+ gem 'hanami-model'
15
+ ```
16
+
17
+ Then we can fetch the dependencies with `bundle install`.
18
+
19
+ ## Setup
20
+
21
+ <a name="connection-url"></a>
22
+
23
+ **Hanami::Model** doesn't have migrations.
24
+ For this example we will use [Sequel](http://sequel.jeremyevans.net).
25
+ We create the database first.
26
+ Then we create two tables: `authors` and `articles`.
27
+
28
+ ```ruby
29
+ require 'bundler/setup'
30
+ require 'sqlite3'
31
+ require 'hanami/model'
32
+ require 'hanami/model/adapters/sql_adapter'
33
+
34
+ connection_uri = "sqlite://#{ __dir__ }/test.db"
35
+
36
+ database = Sequel.connect(connection_uri)
37
+
38
+ database.create_table! :authors do
39
+ primary_key :id
40
+ String :name
41
+ end
42
+
43
+ database.create_table! :articles do
44
+ primary_key :id
45
+ Integer :author_id, null: false
46
+ String :title
47
+ Integer :comments_count, default: 0
48
+ Boolean :published, default: false
49
+ end
50
+ ```
51
+
52
+ ## Entities
53
+
54
+ We have two entities in our application: `Author` and `Article`.
55
+ `Author` is a `Struct`, Hanami::Model can persist it.
56
+ `Article` has a small API concerning its publishing process.
57
+
58
+ ```ruby
59
+ Author = Struct.new(:id, :name) do
60
+ def initialize(attributes = {})
61
+ @id, @name = attributes.values_at(:id, :name)
62
+ end
63
+ end
64
+
65
+ class Article
66
+ include Hanami::Entity
67
+ attributes :author_id, :title, :comments_count, :published # id is implicit
68
+
69
+ def published?
70
+ !!published
71
+ end
72
+
73
+ def publish!
74
+ @published = true
75
+ end
76
+ end
77
+ ```
78
+
79
+ ## Repositories
80
+
81
+ In order to persist and query the entities above, we define two corresponding repositories:
82
+
83
+ ```ruby
84
+ class AuthorRepository
85
+ include Hanami::Repository
86
+ end
87
+
88
+ class ArticleRepository
89
+ include Hanami::Repository
90
+
91
+ def self.most_recent_by_author(author, limit = 8)
92
+ query do
93
+ where(author_id: author.id).
94
+ desc(:id).
95
+ limit(limit)
96
+ end
97
+ end
98
+
99
+ def self.most_recent_published_by_author(author, limit = 8)
100
+ most_recent_by_author(author, limit).published
101
+ end
102
+
103
+ def self.published
104
+ query do
105
+ where(published: true)
106
+ end
107
+ end
108
+
109
+ def self.drafts
110
+ exclude published
111
+ end
112
+
113
+ def self.rank
114
+ published.desc(:comments_count)
115
+ end
116
+
117
+ def self.best_article_ever
118
+ rank.limit(1).first
119
+ end
120
+
121
+ def self.comments_average
122
+ query.average(:comments_count)
123
+ end
124
+ end
125
+ ```
126
+
127
+ ## Loading
128
+
129
+ ```ruby
130
+ Hanami::Model.configure do
131
+ adapter type: :sql, uri: connection_uri
132
+
133
+ mapping do
134
+ collection :authors do
135
+ entity Author
136
+ repository AuthorRepository
137
+
138
+ attribute :id, Integer
139
+ attribute :name, String
140
+ end
141
+
142
+ collection :articles do
143
+ entity Article
144
+ repository ArticleRepository
145
+
146
+ attribute :id, Integer
147
+ attribute :author_id, Integer
148
+ attribute :title, String
149
+ attribute :comments_count, Integer
150
+ attribute :published, Boolean
151
+ end
152
+ end
153
+ end.load!
154
+ ```
155
+
156
+ ## Persist
157
+
158
+ We instantiate and persist an `Author` and a few `Articles` for our example:
159
+
160
+ ```ruby
161
+ author = Author.new(name: 'Luca')
162
+ AuthorRepository.create(author)
163
+
164
+ articles = [
165
+ Article.new(title: 'Announcing Hanami', author_id: author.id, comments_count: 123, published: true),
166
+ Article.new(title: 'Introducing Hanami::Router', author_id: author.id, comments_count: 63, published: true),
167
+ Article.new(title: 'Introducing Hanami::Controller', author_id: author.id, comments_count: 82, published: true),
168
+ Article.new(title: 'Introducing Hanami::Model', author_id: author.id)
169
+ ]
170
+
171
+ articles.each do |article|
172
+ ArticleRepository.create(article)
173
+ end
174
+ ```
175
+
176
+ ## Query
177
+
178
+ We use the repositories to query the database and return the entities we're looking for:
179
+
180
+ ```ruby
181
+ ArticleRepository.first # => return the first article
182
+ ArticleRepository.last # => return the last article
183
+
184
+ ArticleRepository.published # => return all the published articles
185
+ ArticleRepository.drafts # => return all the drafts
186
+
187
+ ArticleRepository.rank # => all the published articles, sorted by popularity
188
+
189
+ ArticleRepository.best_article_ever # => the most commented article
190
+
191
+ ArticleRepository.comments_average # => calculates the average of comments across all the published articles.
192
+
193
+ ArticleRepository.most_recent_by_author(author) # => most recent articles by an author (drafts and published).
194
+ ArticleRepository.most_recent_published_by_author(author) # => most recent published articles by an author
195
+ ```
196
+
197
+ ## Business Logic
198
+
199
+ As we've seen above, `Article` implements an API for publishing.
200
+ We use that logic to alter the state of an article (from draft to published).
201
+ We then use the repository to persist this new state.
202
+
203
+ ```ruby
204
+ article = ArticleRepository.drafts.first
205
+
206
+ article.published? # => false
207
+ article.publish!
208
+
209
+ article.published? # => true
210
+
211
+ ArticleRepository.update(article)
212
+ ```
@@ -0,0 +1,22 @@
1
+ Copyright © 2014-2016 Luca Guidi
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,8 +1,41 @@
1
1
  # Hanami::Model
2
2
 
3
- 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.
3
+ A persistence framework for [Hanami](http://hanamirb.org).
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ It delivers a convenient public API to execute queries and commands against a database.
6
+ The architecture eases keeping the business logic (entities) separated from details such as persistence or validations.
7
+
8
+ It implements the following concepts:
9
+
10
+ * [Entity](#entities) - An object defined by its identity.
11
+ * [Repository](#repositories) - An object that mediates between the entities and the persistence layer.
12
+ * [Data Mapper](#data-mapper) - A persistence mapper that keep entities independent from database details.
13
+ * [Adapter](#adapter) – A database adapter.
14
+ * [Query](#query) - An object that represents a database query.
15
+
16
+ Like all the other Hanami components, it can be used as a standalone framework or within a full Hanami application.
17
+
18
+ ## Status
19
+
20
+ [![Gem Version](https://badge.fury.io/rb/hanami-model.svg)](http://badge.fury.io/rb/hanami-model)
21
+ [![Build Status](https://secure.travis-ci.org/hanami/model.svg?branch=master)](http://travis-ci.org/hanami/model?branch=master)
22
+ [![Coverage](https://img.shields.io/coveralls/hanami/model/master.svg)](https://coveralls.io/r/hanami/model)
23
+ [![Code Climate](https://img.shields.io/codeclimate/github/hanami/model.svg)](https://codeclimate.com/github/hanami/model)
24
+ [![Dependencies](https://gemnasium.com/hanami/model.svg)](https://gemnasium.com/hanami/model)
25
+ [![Inline docs](http://inch-ci.org/github/hanami/model.png)](http://inch-ci.org/github/hanami/model)
26
+
27
+ ## Contact
28
+
29
+ * Home page: http://hanamirb.org
30
+ * Mailing List: http://hanamirb.org/mailing-list
31
+ * API Doc: http://rdoc.info/gems/hanami-model
32
+ * Bugs/Issues: https://github.com/hanami/model/issues
33
+ * Support: http://stackoverflow.com/questions/tagged/hanami
34
+ * Chat: https://chat.hanamirb.org
35
+
36
+ ## Rubies
37
+
38
+ __Hanami::Model__ supports Ruby (MRI) 2.2+ and JRuby 9000+
6
39
 
7
40
  ## Installation
8
41
 
@@ -22,15 +55,575 @@ Or install it yourself as:
22
55
 
23
56
  ## Usage
24
57
 
25
- TODO: Write usage instructions here
58
+ This class provides a DSL to configure adapter, mapping and collection.
59
+
60
+ ```ruby
61
+ require 'hanami/model'
62
+
63
+ class User
64
+ include Hanami::Entity
65
+ attributes :name, :age
66
+ end
67
+
68
+ class UserRepository
69
+ include Hanami::Repository
70
+ end
71
+
72
+ Hanami::Model.configure do
73
+ adapter type: :sql, uri: 'postgres://localhost/database'
74
+
75
+ mapping do
76
+ collection :users do
77
+ entity User
78
+ repository UserRepository
79
+
80
+ attribute :id, Integer
81
+ attribute :name, String
82
+ attribute :age, Integer
83
+ end
84
+ end
85
+ end
86
+
87
+ Hanami::Model.load!
88
+
89
+ user = User.new(name: 'Luca', age: 32)
90
+ user = UserRepository.create(user)
91
+
92
+ puts user.id # => 1
93
+
94
+ u = UserRepository.find(user.id)
95
+ u == user # => true
96
+ ```
97
+
98
+ ## Concepts
99
+
100
+ ### Entities
101
+
102
+ An object that is defined by its identity.
103
+ See "Domain Driven Design" by Eric Evans.
104
+
105
+ An entity is the core of an application, where the part of the domain logic is implemented.
106
+ It's a small, cohesive object that expresses coherent and meaningful behaviors.
107
+
108
+ It deals with one and only one responsibility that is pertinent to the
109
+ domain of the application, without caring about details such as persistence
110
+ or validations.
111
+
112
+ This simplicity of design allows developers to focus on behaviors, or
113
+ message passing if you will, which is the quintessence of Object Oriented Programming.
114
+
115
+ ```ruby
116
+ require 'hanami/model'
117
+
118
+ class Person
119
+ include Hanami::Entity
120
+ attributes :name, :age
121
+ end
122
+ ```
123
+
124
+ When a class includes `Hanami::Entity` it receives the following interface:
125
+
126
+ * `#id`
127
+ * `#id=`
128
+ * `#initialize(attributes = {})`
129
+
130
+ `Hanami::Entity` also provides the `.attributes` for defining attribute accessors for the given names.
131
+
132
+ If we expand the code above in **pure Ruby**, it would be:
133
+
134
+ ```ruby
135
+ class Person
136
+ attr_accessor :id, :name, :age
137
+
138
+ def initialize(attributes = {})
139
+ @id, @name, @age = attributes.values_at(:id, :name, :age)
140
+ end
141
+ end
142
+ ```
143
+
144
+ **Hanami::Model** ships `Hanami::Entity` for developers's convenience.
145
+
146
+ **Hanami::Model** depends on a narrow and well-defined interface for an Entity - `#id`, `#id=`, `#initialize(attributes={})`.
147
+ If your object implements that interface then that object can be used as an Entity in the **Hanami::Model** framework.
148
+
149
+ However, we suggest to implement this interface by including `Hanami::Entity`, in case that future versions of the framework will expand it.
150
+
151
+ See [Dependency Inversion Principle](http://en.wikipedia.org/wiki/Dependency_inversion_principle) for more on interfaces.
152
+
153
+ When a class extends a `Hanami::Entity` class, it will also *inherit* its mother's attributes.
154
+
155
+ ```ruby
156
+ require 'hanami/model'
157
+
158
+ class Article
159
+ include Hanami::Entity
160
+ attributes :name
161
+ end
162
+
163
+ class RareArticle < Article
164
+ attributes :price
165
+ end
166
+ ```
26
167
 
27
- ## Development
168
+ That is, `RareArticle`'s attributes carry over `:name` attribute from `Article`,
169
+ thus is `:id, :name, :price`.
28
170
 
29
- 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.
171
+ ### Repositories
172
+
173
+ An object that mediates between entities and the persistence layer.
174
+ It offers a standardized API to query and execute commands on a database.
175
+
176
+ A repository is **storage independent**, all the queries and commands are
177
+ delegated to the current adapter.
178
+
179
+ This architecture has several advantages:
180
+
181
+ * Applications depend on a standard API, instead of low level details
182
+ (Dependency Inversion principle)
183
+
184
+ * Applications depend on a stable API, that doesn't change if the
185
+ storage changes
186
+
187
+ * Developers can postpone storage decisions
188
+
189
+ * Confines persistence logic at a low level
190
+
191
+ * Multiple data sources can easily coexist in an application
192
+
193
+ When a class includes `Hanami::Repository`, it will receive the following interface:
194
+
195
+ * `.persist(entity)` – Create or update an entity
196
+ * `.create(entity)` – Create a record for the given entity
197
+ * `.update(entity)` – Update the record corresponding to the given entity
198
+ * `.delete(entity)` – Delete the record corresponding to the given entity
199
+ * `.all` - Fetch all the entities from the collection
200
+ * `.find` - Fetch an entity from the collection by its ID
201
+ * `.first` - Fetch the first entity from the collection
202
+ * `.last` - Fetch the last entity from the collection
203
+ * `.clear` - Delete all the records from the collection
204
+ * `.query` - Fabricates a query object
205
+
206
+ **A collection is a homogenous set of records.**
207
+ It corresponds to a table for a SQL database or to a MongoDB collection.
208
+
209
+ **All the queries are private**.
210
+ This decision forces developers to define intention revealing API, instead of leaking storage API details outside of a repository.
211
+
212
+ Look at the following code:
213
+
214
+ ```ruby
215
+ ArticleRepository.where(author_id: 23).order(:published_at).limit(8)
216
+ ```
217
+
218
+ This is **bad** for a variety of reasons:
219
+
220
+ * The caller has an intimate knowledge of the internal mechanisms of the Repository.
221
+
222
+ * The caller works on several levels of abstraction.
223
+
224
+ * It doesn't express a clear intent, it's just a chain of methods.
225
+
226
+ * The caller can't be easily tested in isolation.
227
+
228
+ * If we change the storage, we are forced to change the code of the caller(s).
229
+
230
+ There is a better way:
231
+
232
+ ```ruby
233
+ require 'hanami/model'
234
+
235
+ class ArticleRepository
236
+ include Hanami::Repository
237
+
238
+ def self.most_recent_by_author(author, limit = 8)
239
+ query do
240
+ where(author_id: author.id).
241
+ order(:published_at)
242
+ end.limit(limit)
243
+ end
244
+ end
245
+ ```
246
+
247
+ This is a **huge improvement**, because:
248
+
249
+ * The caller doesn't know how the repository fetches the entities.
250
+
251
+ * The caller works on a single level of abstraction. It doesn't even know about records, only works with entities.
252
+
253
+ * It expresses a clear intent.
254
+
255
+ * The caller can be easily tested in isolation. It's just a matter of stubbing this method.
256
+
257
+ * If we change the storage, the callers aren't affected.
258
+
259
+ Here is an extended example of a repository that uses the SQL adapter.
260
+
261
+ ```ruby
262
+ class ArticleRepository
263
+ include Hanami::Repository
264
+
265
+ def self.most_recent_by_author(author, limit = 8)
266
+ query do
267
+ where(author_id: author.id).
268
+ desc(:id).
269
+ limit(limit)
270
+ end
271
+ end
272
+
273
+ def self.most_recent_published_by_author(author, limit = 8)
274
+ most_recent_by_author(author, limit).published
275
+ end
276
+
277
+ def self.published
278
+ query do
279
+ where(published: true)
280
+ end
281
+ end
282
+
283
+ def self.drafts
284
+ exclude published
285
+ end
286
+
287
+ def self.rank
288
+ published.desc(:comments_count)
289
+ end
290
+
291
+ def self.best_article_ever
292
+ rank.limit(1)
293
+ end
294
+
295
+ def self.comments_average
296
+ query.average(:comments_count)
297
+ end
298
+ end
299
+ ```
300
+
301
+ You can also extract the common logic from your repository into a module to reuse it in other repositories. Here is a pagination example:
302
+
303
+ ```ruby
304
+ module RepositoryHelpers
305
+ module Pagination
306
+ def paginate(limit: 10, offset: 0)
307
+ query do
308
+ limit(limit).offset(offset)
309
+ end
310
+ end
311
+ end
312
+ end
30
313
 
31
- 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).
314
+ class ArticleRepository
315
+ include Hanami::Repository
316
+ extend RepositoryHelpers::Pagination
317
+
318
+ def self.published
319
+ query do
320
+ where(published: true)
321
+ end
322
+ end
323
+
324
+ # other repository-specific methods here
325
+ end
326
+ ```
327
+
328
+ That will allow `.paginate` usage on `ArticleRepository`, for example:
329
+ `ArticleRepository.published.paginate(15, 0)`
330
+
331
+ **Your models and repositories have to be in the same namespace.** Otherwise `Hanami::Model::Mapper#load!`
332
+ will not initialize your repositories correctly.
333
+
334
+ ```ruby
335
+ class MyHanamiApp::Model::User
336
+ include Hanami::Entity
337
+ # your code here
338
+ end
339
+
340
+ # This repository will work...
341
+ class MyHanamiApp::Model::UserRepository
342
+ include Hanami::Repository
343
+ # your code here
344
+ end
345
+
346
+ # ...this will not!
347
+ class MyHanamiApp::Repository::UserRepository
348
+ include Hanami::Repository
349
+ # your code here
350
+ end
351
+ ```
352
+
353
+ ### Data Mapper
354
+
355
+ A persistence mapper that keeps entities independent from database details.
356
+ It is database independent, it can work with SQL, document, and even with key/value stores.
357
+
358
+ The role of a data mapper is to translate database columns into the corresponding attribute of an entity.
359
+
360
+ ```ruby
361
+ require 'hanami/model'
362
+
363
+ mapper = Hanami::Model::Mapper.new do
364
+ collection :users do
365
+ entity User
366
+
367
+ attribute :id, Integer
368
+ attribute :name, String
369
+ attribute :age, Integer
370
+ end
371
+ end
372
+ ```
373
+
374
+ For simplicity's sake, imagine that the mapper above is used with a SQL database.
375
+ 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.
376
+ In the end, each call to `#attribute` associates the specified column with a corresponding Ruby type.
377
+
378
+ For advanced mapping and legacy databases, please have a look at the API doc.
379
+
380
+ **Known limitations**
381
+
382
+ Note there are limitations with inherited entities:
383
+
384
+ ```ruby
385
+ require 'hanami/model'
386
+
387
+ class Article
388
+ include Hanami::Entity
389
+ attributes :name
390
+ end
391
+
392
+ class RareArticle < Article
393
+ attributes :price
394
+ end
395
+
396
+ mapper = Hanami::Model::Mapper.new do
397
+ collection :articles do
398
+ entity Article
399
+
400
+ attribute :id, Integer
401
+ attribute :name, String
402
+ attribute :price, Integer
403
+ end
404
+ end
405
+ ```
406
+
407
+ In the example above, there are a few problems:
408
+
409
+ * `Article` could not be fetched because mapping could not map `price`.
410
+ * Finding a persisted `RareArticle` record, for eg. `ArticleRepository.find(123)`,
411
+ the result is an `Article` not `RareArticle`.
412
+
413
+ ### Adapter
414
+
415
+ An adapter is a concrete implementation of persistence logic for a specific database.
416
+ **Hanami::Model** is shipped with three adapters:
417
+
418
+ * SqlAdapter
419
+ * MemoryAdapter
420
+ * FileSystemAdapter
421
+
422
+ An adapter can be associated with one or multiple repositories.
423
+
424
+ ```ruby
425
+ require 'pg'
426
+ require 'hanami/model'
427
+ require 'hanami/model/adapters/sql_adapter'
428
+
429
+ mapper = Hanami::Model::Mapper.new do
430
+ # ...
431
+ end
432
+
433
+ adapter = Hanami::Model::Adapters::SqlAdapter.new(mapper, 'postgres://host:port/database')
434
+
435
+ PersonRepository.adapter = adapter
436
+ ArticleRepository.adapter = adapter
437
+ ```
438
+
439
+ In the example above, we reuse the adapter because the target tables (`people` and `articles`) are defined in the same database.
440
+ **As rule of thumb, one adapter instance per database.**
441
+
442
+ ### Query
443
+
444
+ An object that implements an interface for querying the database.
445
+ This interface may vary, according to the adapter's specifications.
446
+
447
+ Here is common interface for existing class:
448
+
449
+ * `.all` - Resolves the query by fetching records from the database and translating them into entities
450
+ * `.where`, `.and` - Adds a condition that behaves like SQL `WHERE`
451
+ * `.or` - Adds a condition that behaves like SQL `OR`
452
+ * `.exclude`, `.not` - Logical negation of a #where condition
453
+ * `.select` - Selects only the specified columns
454
+ * `.order`, `.asc` - Specify the ascending order of the records, sorted by the given columns
455
+ * `.reverse_order`, `.desc` - Specify the descending order of the records, sorted by the given columns
456
+ * `.limit` - Limit the number of records to return
457
+ * `.offset` - Specify an `OFFSET` clause. Due to SQL syntax restriction, offset MUST be used with `#limit`
458
+ * `.sum` - Returns the sum of the values for the given column
459
+ * `.average`, `.avg` - Returns the average of the values for the given column
460
+ * `.max` - Returns the maximum value for the given column
461
+ * `.min` - Returns the minimum value for the given column
462
+ * `.interval` - Returns the difference between the MAX and MIN for the given column
463
+ * `.range` - Returns a range of values between the MAX and the MIN for the given column
464
+ * `.exist?` - Checks if at least one record exists for the current conditions
465
+ * `.count` - Returns a count of the records for the current conditions
466
+ * `.join` - Adds an inner join with a table (only SQL)
467
+ * `.left_join` - Adds a left join with a table (only SQL)
468
+
469
+ 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.
470
+
471
+ Think of an adapter for Redis, it will probably employ different strategies to filter records than an SQL query object.
472
+
473
+ ### Conventions
474
+
475
+ * A repository must be named after an entity, by appending `"Repository"` to the entity class name (eg. `Article` => `ArticleRepository`).
476
+
477
+ ### Configurations
478
+
479
+ * Non-standard repository can be configured for an entity, by setting `repository` on the collection.
480
+
481
+ ```ruby
482
+ require 'hanami/model'
483
+
484
+ mapper = Hanami::Model::Mapper.new do
485
+ collection :users do
486
+ entity User
487
+ repository EmployeeRepository
488
+ end
489
+ end
490
+ ```
491
+
492
+ ### Thread safety
493
+
494
+ **Hanami::Model**'s is thread safe during the runtime, but it isn't during the loading process.
495
+ The mapper compiles some code internally, be sure to safely load it before your application starts.
496
+
497
+ ```ruby
498
+ Mutex.new.synchronize do
499
+ Hanami::Model.load!
500
+ end
501
+ ```
502
+
503
+ **This is not necessary, when Hanami::Model is used within a Hanami application.**
504
+
505
+ ## Features
506
+
507
+ ### Timestamps
508
+
509
+ If an entity has the following accessors: `:created_at` and `:updated_at`, they will be automatically updated when the entity is persisted.
510
+
511
+ ```ruby
512
+ require 'hanami/model'
513
+
514
+ class User
515
+ include Hanami::Entity
516
+ attributes :name, :created_at, :updated_at
517
+ end
518
+
519
+ class UserRepository
520
+ include Hanami::Repository
521
+ end
522
+
523
+ Hanami::Model.configure do
524
+ adapter type: :memory, uri: 'memory://localhost/timestamps'
525
+
526
+ mapping do
527
+ collection :users do
528
+ entity User
529
+ repository UserRepository
530
+
531
+ attribute :id, Integer
532
+ attribute :name, String
533
+ attribute :created_at, DateTime
534
+ attribute :updated_at, DateTime
535
+ end
536
+ end
537
+ end.load!
538
+
539
+ user = User.new(name: 'L')
540
+ puts user.created_at # => nil
541
+ puts user.updated_at # => nil
542
+
543
+ user = UserRepository.create(user)
544
+ puts user.created_at.to_s # => "2015-05-15T10:12:20+00:00"
545
+ puts user.updated_at.to_s # => "2015-05-15T10:12:20+00:00"
546
+
547
+ sleep 3
548
+ user.name = "Luca"
549
+ user = UserRepository.update(user)
550
+ puts user.created_at.to_s # => "2015-05-15T10:12:20+00:00"
551
+ puts user.updated_at.to_s # => "2015-05-15T10:12:23+00:00"
552
+ ```
553
+
554
+ ### Dirty Tracking
555
+
556
+ Entities are able to track changes of their data, if `Hanami::Entity::DirtyTracking` is included.
557
+
558
+ ```ruby
559
+ require 'hanami/model'
560
+
561
+ class User
562
+ include Hanami::Entity
563
+ include Hanami::Entity::DirtyTracking
564
+ attributes :name, :age
565
+ end
566
+
567
+ class UserRepository
568
+ include Hanami::Repository
569
+ end
570
+
571
+ Hanami::Model.configure do
572
+ adapter type: :memory, uri: 'memory://localhost/dirty_tracking'
573
+
574
+ mapping do
575
+ collection :users do
576
+ entity User
577
+ repository UserRepository
578
+
579
+ attribute :id, Integer
580
+ attribute :name, String
581
+ attribute :age, String
582
+ end
583
+ end
584
+ end.load!
585
+
586
+ user = User.new(name: 'L')
587
+ user.changed? # => false
588
+
589
+ user.age = 33
590
+ user.changed? # => true
591
+ user.changed_attributes # => {:age=>33}
592
+
593
+ user = UserRepository.create(user)
594
+ user.changed? # => false
595
+
596
+ user.update(name: 'Luca')
597
+ user.changed? # => true
598
+ user.changed_attributes # => {:name=>"Luca"}
599
+
600
+ user = UserRepository.update(user)
601
+ user.changed? # => false
602
+
603
+ result = UserRepository.find(user.id)
604
+ result.changed? # => false
605
+ ```
606
+
607
+ ## Example
608
+
609
+ For a full working example, have a look at [EXAMPLE.md](https://github.com/hanami/model/blob/master/EXAMPLE.md).
610
+ Please remember that the setup code is only required for the standalone usage of **Hanami::Model**.
611
+ A **Hanami** application will handle that configurations for you.
612
+
613
+ ## Versioning
614
+
615
+ __Hanami::Model__ uses [Semantic Versioning 2.0.0](http://semver.org)
32
616
 
33
617
  ## Contributing
34
618
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/hanami-model.
619
+ 1. Fork it ( https://github.com/hanami/model/fork )
620
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
621
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
622
+ 4. Push to the branch (`git push origin my-new-feature`)
623
+ 5. Create new Pull Request
624
+
625
+ ## Copyright
626
+
627
+ Copyright © 2014-2016 Luca Guidi – Released under MIT License
36
628
 
629
+ This project was formerly known as Lotus (`lotus-model`).