hanami-model 0.6.1 → 0.7.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 +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)
|