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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +145 -0
- data/EXAMPLE.md +212 -0
- data/LICENSE.md +22 -0
- data/README.md +600 -7
- data/hanami-model.gemspec +17 -12
- data/lib/hanami-model.rb +1 -0
- data/lib/hanami/entity.rb +298 -0
- data/lib/hanami/entity/dirty_tracking.rb +74 -0
- data/lib/hanami/model.rb +204 -2
- data/lib/hanami/model/adapters/abstract.rb +281 -0
- data/lib/hanami/model/adapters/file_system_adapter.rb +288 -0
- data/lib/hanami/model/adapters/implementation.rb +111 -0
- data/lib/hanami/model/adapters/memory/collection.rb +132 -0
- data/lib/hanami/model/adapters/memory/command.rb +113 -0
- data/lib/hanami/model/adapters/memory/query.rb +653 -0
- data/lib/hanami/model/adapters/memory_adapter.rb +179 -0
- data/lib/hanami/model/adapters/null_adapter.rb +24 -0
- data/lib/hanami/model/adapters/sql/collection.rb +287 -0
- data/lib/hanami/model/adapters/sql/command.rb +73 -0
- data/lib/hanami/model/adapters/sql/console.rb +33 -0
- data/lib/hanami/model/adapters/sql/consoles/mysql.rb +49 -0
- data/lib/hanami/model/adapters/sql/consoles/postgresql.rb +48 -0
- data/lib/hanami/model/adapters/sql/consoles/sqlite.rb +26 -0
- data/lib/hanami/model/adapters/sql/query.rb +788 -0
- data/lib/hanami/model/adapters/sql_adapter.rb +296 -0
- data/lib/hanami/model/coercer.rb +74 -0
- data/lib/hanami/model/config/adapter.rb +116 -0
- data/lib/hanami/model/config/mapper.rb +45 -0
- data/lib/hanami/model/configuration.rb +275 -0
- data/lib/hanami/model/error.rb +7 -0
- data/lib/hanami/model/mapper.rb +124 -0
- data/lib/hanami/model/mapping.rb +48 -0
- data/lib/hanami/model/mapping/attribute.rb +85 -0
- data/lib/hanami/model/mapping/coercers.rb +314 -0
- data/lib/hanami/model/mapping/collection.rb +490 -0
- data/lib/hanami/model/mapping/collection_coercer.rb +79 -0
- data/lib/hanami/model/migrator.rb +324 -0
- data/lib/hanami/model/migrator/adapter.rb +170 -0
- data/lib/hanami/model/migrator/connection.rb +133 -0
- data/lib/hanami/model/migrator/mysql_adapter.rb +72 -0
- data/lib/hanami/model/migrator/postgres_adapter.rb +119 -0
- data/lib/hanami/model/migrator/sqlite_adapter.rb +110 -0
- data/lib/hanami/model/version.rb +4 -1
- data/lib/hanami/repository.rb +872 -0
- metadata +100 -16
- data/.gitignore +0 -9
- data/Gemfile +0 -4
- data/Rakefile +0 -2
- data/bin/console +0 -14
- data/bin/setup +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a99014d711cf7b1e4fd9469acdee5a7fefb69d9f
|
4
|
+
data.tar.gz: 71781d9d98ea8e5b5e75bd0df4527ed47f6e6b3a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 312c76418b3d0e8a8c7c41125c009f03bfdd03c4102de60ed87806df705c9584922ff1afc952b6ae383a86805b209333c1ba31dbfb4706810028bcba598e88f9
|
7
|
+
data.tar.gz: 5bba6c44f8cebef794b53c7b7a94d897f09098440feec568f293602fc46e0593c52ce090451cdcaee76e261fc0a908d731aa82ab552afb172b078f07e3be46ba
|
data/CHANGELOG.md
ADDED
@@ -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`
|
data/EXAMPLE.md
ADDED
@@ -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
|
+
```
|
data/LICENSE.md
ADDED
@@ -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
|
-
|
3
|
+
A persistence framework for [Hanami](http://hanamirb.org).
|
4
4
|
|
5
|
-
|
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
|
+
[](http://badge.fury.io/rb/hanami-model)
|
21
|
+
[](http://travis-ci.org/hanami/model?branch=master)
|
22
|
+
[](https://coveralls.io/r/hanami/model)
|
23
|
+
[](https://codeclimate.com/github/hanami/model)
|
24
|
+
[](https://gemnasium.com/hanami/model)
|
25
|
+
[](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
|
-
|
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
|
-
|
168
|
+
That is, `RareArticle`'s attributes carry over `:name` attribute from `Article`,
|
169
|
+
thus is `:id, :name, :price`.
|
28
170
|
|
29
|
-
|
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
|
-
|
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
|
-
|
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`).
|