hanami-model 0.6.1 → 0.7.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 +44 -0
- data/README.md +54 -420
- data/hanami-model.gemspec +9 -6
- data/lib/hanami/entity.rb +107 -191
- data/lib/hanami/entity/schema.rb +236 -0
- data/lib/hanami/model.rb +52 -138
- data/lib/hanami/model/association.rb +37 -0
- data/lib/hanami/model/associations/belongs_to.rb +19 -0
- data/lib/hanami/model/associations/dsl.rb +29 -0
- data/lib/hanami/model/associations/has_many.rb +200 -0
- data/lib/hanami/model/configuration.rb +52 -224
- data/lib/hanami/model/configurator.rb +62 -0
- data/lib/hanami/model/entity_name.rb +35 -0
- data/lib/hanami/model/error.rb +37 -24
- data/lib/hanami/model/mapping.rb +29 -35
- data/lib/hanami/model/migration.rb +31 -0
- data/lib/hanami/model/migrator.rb +111 -88
- data/lib/hanami/model/migrator/adapter.rb +39 -16
- data/lib/hanami/model/migrator/connection.rb +23 -11
- data/lib/hanami/model/migrator/mysql_adapter.rb +38 -17
- data/lib/hanami/model/migrator/postgres_adapter.rb +20 -19
- data/lib/hanami/model/migrator/sqlite_adapter.rb +9 -8
- data/lib/hanami/model/plugins.rb +25 -0
- data/lib/hanami/model/plugins/mapping.rb +55 -0
- data/lib/hanami/model/plugins/schema.rb +55 -0
- data/lib/hanami/model/plugins/timestamps.rb +118 -0
- data/lib/hanami/model/relation_name.rb +24 -0
- data/lib/hanami/model/sql.rb +161 -0
- data/lib/hanami/model/sql/console.rb +41 -0
- data/lib/hanami/model/sql/consoles/abstract.rb +33 -0
- data/lib/hanami/model/sql/consoles/mysql.rb +63 -0
- data/lib/hanami/model/sql/consoles/postgresql.rb +68 -0
- data/lib/hanami/model/sql/consoles/sqlite.rb +46 -0
- data/lib/hanami/model/sql/entity/schema.rb +125 -0
- data/lib/hanami/model/sql/types.rb +95 -0
- data/lib/hanami/model/sql/types/schema/coercions.rb +198 -0
- data/lib/hanami/model/types.rb +99 -0
- data/lib/hanami/model/version.rb +1 -1
- data/lib/hanami/repository.rb +287 -723
- metadata +77 -40
- data/EXAMPLE.md +0 -213
- data/lib/hanami/entity/dirty_tracking.rb +0 -74
- data/lib/hanami/model/adapters/abstract.rb +0 -281
- data/lib/hanami/model/adapters/file_system_adapter.rb +0 -288
- data/lib/hanami/model/adapters/implementation.rb +0 -111
- data/lib/hanami/model/adapters/memory/collection.rb +0 -132
- data/lib/hanami/model/adapters/memory/command.rb +0 -113
- data/lib/hanami/model/adapters/memory/query.rb +0 -653
- data/lib/hanami/model/adapters/memory_adapter.rb +0 -179
- data/lib/hanami/model/adapters/null_adapter.rb +0 -24
- data/lib/hanami/model/adapters/sql/collection.rb +0 -287
- data/lib/hanami/model/adapters/sql/command.rb +0 -88
- data/lib/hanami/model/adapters/sql/console.rb +0 -33
- data/lib/hanami/model/adapters/sql/consoles/mysql.rb +0 -49
- data/lib/hanami/model/adapters/sql/consoles/postgresql.rb +0 -48
- data/lib/hanami/model/adapters/sql/consoles/sqlite.rb +0 -26
- data/lib/hanami/model/adapters/sql/query.rb +0 -788
- data/lib/hanami/model/adapters/sql_adapter.rb +0 -296
- data/lib/hanami/model/coercer.rb +0 -74
- data/lib/hanami/model/config/adapter.rb +0 -116
- data/lib/hanami/model/config/mapper.rb +0 -45
- data/lib/hanami/model/mapper.rb +0 -124
- data/lib/hanami/model/mapping/attribute.rb +0 -85
- data/lib/hanami/model/mapping/coercers.rb +0 -314
- data/lib/hanami/model/mapping/collection.rb +0 -490
- data/lib/hanami/model/mapping/collection_coercer.rb +0 -79
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ac96a7e41555f5a379c12d3fd45c999ea4ce7e93
|
4
|
+
data.tar.gz: 59f4d8fe809cd3536bc47fff73830ba37304a6c7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f730a23b1a29d426405c1cb79ee2ea74ab73f9baaa73521c7136b3f39913a4a9dcc6cdeed2e45ce4ffd2b334fbb839e637b8ff60edbafa19777d47fb9e5c505d
|
7
|
+
data.tar.gz: 382a94c09b68ed787d67643b6128ce1d2f54b0d2c669bc2c0248ad45c8e54fbfe14bd613bb9e9268988d16dcc29f2dd05c30be08c7461ec1a30b11fe34f9f635
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,50 @@
|
|
1
1
|
# Hanami::Model
|
2
2
|
A persistence layer for Hanami
|
3
3
|
|
4
|
+
## v0.7.0 - 2016-11-15
|
5
|
+
### Added
|
6
|
+
- [Luca Guidi] `Hanami::Entity` defines an automatic schema for SQL databases
|
7
|
+
– [Luca Guidi] `Hanami::Entity` attributes schema
|
8
|
+
- [Luca Guidi] Experimental support for One-To-Many association (aka `has_many`)
|
9
|
+
- [Luca Guidi] Native support for PostgreSQL types like UUID, Array, JSON(B) and Money
|
10
|
+
- [Luca Guidi] Repositories instances can access all the relations (eg. `BookRepository` can access `users` relation via `#users`)
|
11
|
+
- [Luca Guidi] Automapping for SQL databases
|
12
|
+
- [Luca Guidi] Added `Hanami::Model::DatabaseError`
|
13
|
+
|
14
|
+
### Changed
|
15
|
+
- [Luca Guidi] Entities are immutable
|
16
|
+
- [Luca Guidi] Removed support for Memory and File System adapters
|
17
|
+
- [Luca Guidi] Removed support for _dirty tracking_
|
18
|
+
- [Luca Guidi] `Hanami::Entity.attributes` method no longer accepts a list of attributes, but a block to optionally define typed attributes
|
19
|
+
- [Luca Guidi] Removed `#fetch`, `#execute` and `#transaction` from repository
|
20
|
+
- [Luca Guidi] Removed `mapping` block from `Hanami::Model.configure`
|
21
|
+
- [Luca Guidi] Changed `adapter` signature in `Hanami::Model.configure` (use `adapter :sql, ENV['DATABASE_URL']`)
|
22
|
+
- [Luca Guidi] Repositories must inherit from `Hanami::Repository` instead of including it
|
23
|
+
- [Luca Guidi] Entities must inherit from `Hanami::Entity` instead of including it
|
24
|
+
- [Pascal Betz] Repositories use instance level interface (eg. `BookRepository.new.find` instead of `BookRepository.find`)
|
25
|
+
- [Luca Guidi] Repositories now accept hashes for CRUD operations
|
26
|
+
- [Luca Guidi] `Hanami::Repository#create` now accepts: hash (or entity)
|
27
|
+
- [Luca Guidi] `Hanami::Repository#update` now accepts two arguments: primary key (`id`) and data (or entity)
|
28
|
+
- [Luca Guidi] `Hanami::Repository#delete` now accepts: primary key (`id`)
|
29
|
+
- [Luca Guidi] Drop `Hanami::Model::NonPersistedEntityError`, `Hanami::Model::InvalidMappingError`, `Hanami::Model::InvalidCommandError`, `Hanami::Model::InvalidQueryError`
|
30
|
+
- [Luca Guidi] Official support for Ruby 2.3 and JRuby 9.0.5.0
|
31
|
+
- [Luca Guidi] Drop support for Ruby 2.0, 2.1, 2.2, and JRuby 9.0.0.0
|
32
|
+
- [Luca Guidi] Drop support for `mysql` gem in favor of `mysql2`
|
33
|
+
|
34
|
+
### Fixed
|
35
|
+
- [Luca Guidi] Ensure booleans to be correctly dumped in database
|
36
|
+
- [Luca Guidi] Ensure to respect default database schema values
|
37
|
+
- [Luca Guidi] Ensure SQL UPDATE to not override non-default primary key
|
38
|
+
- [James Hamilton] Print appropriate error message when trying to create a PostgreSQL database that is already existing
|
39
|
+
|
40
|
+
## v0.6.2 - 2016-06-01
|
41
|
+
### Changed
|
42
|
+
- [Kjell-Magne Øierud] Ensure inherited entities to expose attributes from base class
|
43
|
+
|
44
|
+
## v0.6.1 - 2016-02-05
|
45
|
+
### Changed
|
46
|
+
- [Hélio Costa e Silva & Pascal Betz] Mapping SQL Adapter's errors as `Hanami::Model` errors
|
47
|
+
|
4
48
|
## v0.6.1 - 2016-02-05
|
5
49
|
### Changed
|
6
50
|
- [Hélio Costa e Silva & Pascal Betz] Mapping SQL Adapter's errors as `Hanami::Model` errors
|
data/README.md
CHANGED
@@ -7,11 +7,8 @@ The architecture eases keeping the business logic (entities) separated from deta
|
|
7
7
|
|
8
8
|
It implements the following concepts:
|
9
9
|
|
10
|
-
* [Entity](#entities) -
|
10
|
+
* [Entity](#entities) - A model domain object defined by its identity.
|
11
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
12
|
|
16
13
|
Like all the other Hanami components, it can be used as a standalone framework or within a full Hanami application.
|
17
14
|
|
@@ -35,7 +32,7 @@ Like all the other Hanami components, it can be used as a standalone framework o
|
|
35
32
|
|
36
33
|
## Rubies
|
37
34
|
|
38
|
-
__Hanami::Model__ supports Ruby (MRI) 2.
|
35
|
+
__Hanami::Model__ supports Ruby (MRI) 2.3+ and JRuby 9.1.5.0+
|
39
36
|
|
40
37
|
## Installation
|
41
38
|
|
@@ -55,51 +52,40 @@ Or install it yourself as:
|
|
55
52
|
|
56
53
|
## Usage
|
57
54
|
|
58
|
-
This class provides a DSL to configure
|
55
|
+
This class provides a DSL to configure the connection.
|
59
56
|
|
60
57
|
```ruby
|
61
58
|
require 'hanami/model'
|
62
59
|
|
63
|
-
class User
|
64
|
-
include Hanami::Entity
|
65
|
-
attributes :name, :age
|
60
|
+
class User < Hanami::Entity
|
66
61
|
end
|
67
62
|
|
68
|
-
class UserRepository
|
69
|
-
include Hanami::Repository
|
63
|
+
class UserRepository < Hanami::Repository
|
70
64
|
end
|
71
65
|
|
72
66
|
Hanami::Model.configure do
|
73
|
-
adapter
|
67
|
+
adapter :sql, 'postgres://username:password@localhost/bookshelf'
|
68
|
+
end.load!
|
74
69
|
|
75
|
-
|
76
|
-
|
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
|
70
|
+
repository = UserRepository.new
|
71
|
+
user = repository.create(name: 'Luca')
|
86
72
|
|
87
|
-
|
73
|
+
puts user.id # => 1
|
88
74
|
|
89
|
-
|
90
|
-
user
|
75
|
+
found = repository.find(user.id)
|
76
|
+
found == user # => true
|
91
77
|
|
92
|
-
|
78
|
+
updated = repository.update(user.id, age: 34)
|
79
|
+
updated.age # => 34
|
93
80
|
|
94
|
-
|
95
|
-
u == user # => true
|
81
|
+
repository.delete(user.id)
|
96
82
|
```
|
97
83
|
|
98
84
|
## Concepts
|
99
85
|
|
100
86
|
### Entities
|
101
87
|
|
102
|
-
|
88
|
+
A model domain object that is defined by its identity.
|
103
89
|
See "Domain Driven Design" by Eric Evans.
|
104
90
|
|
105
91
|
An entity is the core of an application, where the part of the domain logic is implemented.
|
@@ -115,59 +101,10 @@ message passing if you will, which is the quintessence of Object Oriented Progra
|
|
115
101
|
```ruby
|
116
102
|
require 'hanami/model'
|
117
103
|
|
118
|
-
class Person
|
119
|
-
include Hanami::Entity
|
120
|
-
attributes :name, :age
|
104
|
+
class Person < Hanami::Entity
|
121
105
|
end
|
122
106
|
```
|
123
107
|
|
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
|
-
```
|
167
|
-
|
168
|
-
That is, `RareArticle`'s attributes carry over `:name` attribute from `Article`,
|
169
|
-
thus is `:id, :name, :price`.
|
170
|
-
|
171
108
|
### Repositories
|
172
109
|
|
173
110
|
An object that mediates between entities and the persistence layer.
|
@@ -192,18 +129,16 @@ This architecture has several advantages:
|
|
192
129
|
|
193
130
|
When a class includes `Hanami::Repository`, it will receive the following interface:
|
194
131
|
|
195
|
-
*
|
196
|
-
*
|
197
|
-
*
|
198
|
-
*
|
199
|
-
*
|
200
|
-
*
|
201
|
-
*
|
202
|
-
*
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
**A collection is a homogenous set of records.**
|
132
|
+
* `#create(data)` – Create a record for the given data (or entity)
|
133
|
+
* `#update(id, data)` – Update the record corresponding to the given id by setting the given data (or entity)
|
134
|
+
* `#delete(id)` – Delete the record corresponding to the given id
|
135
|
+
* `#all` - Fetch all the entities from the relation
|
136
|
+
* `#find` - Fetch an entity from the relation by primary key
|
137
|
+
* `#first` - Fetch the first entity from the relation
|
138
|
+
* `#last` - Fetch the last entity from the relation
|
139
|
+
* `#clear` - Delete all the records from the relation
|
140
|
+
|
141
|
+
**A relation is a homogenous set of records.**
|
207
142
|
It corresponds to a table for a SQL database or to a MongoDB collection.
|
208
143
|
|
209
144
|
**All the queries are private**.
|
@@ -212,7 +147,7 @@ This decision forces developers to define intention revealing API, instead of le
|
|
212
147
|
Look at the following code:
|
213
148
|
|
214
149
|
```ruby
|
215
|
-
ArticleRepository.where(author_id: 23).order(:published_at).limit(8)
|
150
|
+
ArticleRepository.new.where(author_id: 23).order(:published_at).limit(8)
|
216
151
|
```
|
217
152
|
|
218
153
|
This is **bad** for a variety of reasons:
|
@@ -232,14 +167,11 @@ There is a better way:
|
|
232
167
|
```ruby
|
233
168
|
require 'hanami/model'
|
234
169
|
|
235
|
-
class ArticleRepository
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
where(author_id: author.id).
|
241
|
-
order(:published_at)
|
242
|
-
end.limit(limit)
|
170
|
+
class ArticleRepository < Hanami::Repository
|
171
|
+
def most_recent_by_author(author, limit: 8)
|
172
|
+
articles.where(author_id: author.id).
|
173
|
+
order(:published_at).
|
174
|
+
limit(limit)
|
243
175
|
end
|
244
176
|
end
|
245
177
|
```
|
@@ -256,253 +188,31 @@ This is a **huge improvement**, because:
|
|
256
188
|
|
257
189
|
* If we change the storage, the callers aren't affected.
|
258
190
|
|
259
|
-
|
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
|
191
|
+
### Mapping
|
290
192
|
|
291
|
-
|
292
|
-
rank.limit(1)
|
293
|
-
end
|
193
|
+
Hanami::Model can **_automap_** columns from relations and entities attributes.
|
294
194
|
|
295
|
-
|
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
|
313
|
-
|
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.
|
195
|
+
However, there are cases where columns and attribute names do not match (mainly **legacy databases**).
|
359
196
|
|
360
197
|
```ruby
|
361
198
|
require 'hanami/model'
|
362
199
|
|
363
|
-
|
364
|
-
|
365
|
-
entity User
|
200
|
+
class UserRepository < Hanami::Repository
|
201
|
+
self.relation = :t_user_archive
|
366
202
|
|
367
|
-
|
368
|
-
attribute :
|
369
|
-
attribute :
|
370
|
-
|
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
|
203
|
+
mapping do
|
204
|
+
attribute :id, from: :i_user_id
|
205
|
+
attribute :name, from: :s_name
|
206
|
+
attribute :age, from: :i_age
|
403
207
|
end
|
404
208
|
end
|
405
209
|
```
|
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
|
-
### Model Error Coercions
|
474
|
-
|
475
|
-
All adapters' errors are encapsulated into Hanami error classes.
|
476
|
-
|
477
|
-
Hanami Model may raise the following exceptions:
|
478
|
-
|
479
|
-
* `Hanami::Model::UniqueConstraintViolationError`
|
480
|
-
* `Hanami::Model::ForeignKeyConstraintViolationError`
|
481
|
-
* `Hanami::Model::NotNullConstraintViolationError`
|
482
|
-
* `Hanami::Model::CheckConstraintViolationError`
|
483
|
-
|
484
|
-
For any other adapter's errors, Hanami will raise the `Hanami::Model::InvalidCommandError` object.
|
485
|
-
All errors contains the root cause and the full error message thrown by sql adapter.
|
210
|
+
**NOTE:** This feature should be used only when **_automapping_** fails because the naming mismatch.
|
486
211
|
|
487
212
|
### Conventions
|
488
213
|
|
489
214
|
* A repository must be named after an entity, by appending `"Repository"` to the entity class name (eg. `Article` => `ArticleRepository`).
|
490
215
|
|
491
|
-
### Configurations
|
492
|
-
|
493
|
-
* Non-standard repository can be configured for an entity, by setting `repository` on the collection.
|
494
|
-
|
495
|
-
```ruby
|
496
|
-
require 'hanami/model'
|
497
|
-
|
498
|
-
mapper = Hanami::Model::Mapper.new do
|
499
|
-
collection :users do
|
500
|
-
entity User
|
501
|
-
repository EmployeeRepository
|
502
|
-
end
|
503
|
-
end
|
504
|
-
```
|
505
|
-
|
506
216
|
### Thread safety
|
507
217
|
|
508
218
|
**Hanami::Model**'s is thread safe during the runtime, but it isn't during the loading process.
|
@@ -525,105 +235,29 @@ If an entity has the following accessors: `:created_at` and `:updated_at`, they
|
|
525
235
|
```ruby
|
526
236
|
require 'hanami/model'
|
527
237
|
|
528
|
-
class User
|
529
|
-
include Hanami::Entity
|
530
|
-
attributes :name, :created_at, :updated_at
|
531
|
-
end
|
532
|
-
|
533
|
-
class UserRepository
|
534
|
-
include Hanami::Repository
|
535
|
-
end
|
536
|
-
|
537
|
-
Hanami::Model.configure do
|
538
|
-
adapter type: :memory, uri: 'memory://localhost/timestamps'
|
539
|
-
|
540
|
-
mapping do
|
541
|
-
collection :users do
|
542
|
-
entity User
|
543
|
-
repository UserRepository
|
544
|
-
|
545
|
-
attribute :id, Integer
|
546
|
-
attribute :name, String
|
547
|
-
attribute :created_at, DateTime
|
548
|
-
attribute :updated_at, DateTime
|
549
|
-
end
|
550
|
-
end
|
551
|
-
end.load!
|
552
|
-
|
553
|
-
user = User.new(name: 'L')
|
554
|
-
puts user.created_at # => nil
|
555
|
-
puts user.updated_at # => nil
|
556
|
-
|
557
|
-
user = UserRepository.create(user)
|
558
|
-
puts user.created_at.to_s # => "2015-05-15T10:12:20+00:00"
|
559
|
-
puts user.updated_at.to_s # => "2015-05-15T10:12:20+00:00"
|
560
|
-
|
561
|
-
sleep 3
|
562
|
-
user.name = "Luca"
|
563
|
-
user = UserRepository.update(user)
|
564
|
-
puts user.created_at.to_s # => "2015-05-15T10:12:20+00:00"
|
565
|
-
puts user.updated_at.to_s # => "2015-05-15T10:12:23+00:00"
|
566
|
-
```
|
567
|
-
|
568
|
-
### Dirty Tracking
|
569
|
-
|
570
|
-
Entities are able to track changes of their data, if `Hanami::Entity::DirtyTracking` is included.
|
571
|
-
|
572
|
-
```ruby
|
573
|
-
require 'hanami/model'
|
574
|
-
|
575
|
-
class User
|
576
|
-
include Hanami::Entity
|
577
|
-
include Hanami::Entity::DirtyTracking
|
578
|
-
attributes :name, :age
|
238
|
+
class User < Hanami::Entity
|
579
239
|
end
|
580
240
|
|
581
|
-
class UserRepository
|
582
|
-
include Hanami::Repository
|
241
|
+
class UserRepository < Hanami::Repository
|
583
242
|
end
|
584
243
|
|
585
244
|
Hanami::Model.configure do
|
586
|
-
adapter
|
587
|
-
|
588
|
-
mapping do
|
589
|
-
collection :users do
|
590
|
-
entity User
|
591
|
-
repository UserRepository
|
592
|
-
|
593
|
-
attribute :id, Integer
|
594
|
-
attribute :name, String
|
595
|
-
attribute :age, String
|
596
|
-
end
|
597
|
-
end
|
245
|
+
adapter :sql, uri: 'postgresql://localhost/bookshelf'
|
598
246
|
end.load!
|
599
247
|
|
600
|
-
|
601
|
-
user.changed? # => false
|
602
|
-
|
603
|
-
user.age = 33
|
604
|
-
user.changed? # => true
|
605
|
-
user.changed_attributes # => {:age=>33}
|
248
|
+
repository = UserRepository.new
|
606
249
|
|
607
|
-
user =
|
608
|
-
user.changed? # => false
|
250
|
+
user = repository.create(name: 'Luca')
|
609
251
|
|
610
|
-
user.
|
611
|
-
user.
|
612
|
-
user.changed_attributes # => {:name=>"Luca"}
|
252
|
+
puts user.created_at.to_s # => "2016-09-19 13:40:13 UTC"
|
253
|
+
puts user.updated_at.to_s # => "2016-09-19 13:40:13 UTC"
|
613
254
|
|
614
|
-
|
615
|
-
user.
|
616
|
-
|
617
|
-
|
618
|
-
result.changed? # => false
|
255
|
+
sleep 3
|
256
|
+
user = repository.update(user.id, age: 34)
|
257
|
+
puts user.created_at.to_s # => "2016-09-19 13:40:13 UTC"
|
258
|
+
puts user.updated_at.to_s # => "2016-09-19 13:40:16 UTC"
|
619
259
|
```
|
620
260
|
|
621
|
-
## Example
|
622
|
-
|
623
|
-
For a full working example, have a look at [EXAMPLE.md](https://github.com/hanami/model/blob/master/EXAMPLE.md).
|
624
|
-
Please remember that the setup code is only required for the standalone usage of **Hanami::Model**.
|
625
|
-
A **Hanami** application will handle that configurations for you.
|
626
|
-
|
627
261
|
## Versioning
|
628
262
|
|
629
263
|
__Hanami::Model__ uses [Semantic Versioning 2.0.0](http://semver.org)
|