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.
- 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
|
+
[![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
|
-
|
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`).
|