hanami-model 0.0.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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`).