hanami-model 0.0.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +145 -0
- data/EXAMPLE.md +212 -0
- data/LICENSE.md +22 -0
- data/README.md +600 -7
- data/hanami-model.gemspec +17 -12
- data/lib/hanami-model.rb +1 -0
- data/lib/hanami/entity.rb +298 -0
- data/lib/hanami/entity/dirty_tracking.rb +74 -0
- data/lib/hanami/model.rb +204 -2
- data/lib/hanami/model/adapters/abstract.rb +281 -0
- data/lib/hanami/model/adapters/file_system_adapter.rb +288 -0
- data/lib/hanami/model/adapters/implementation.rb +111 -0
- data/lib/hanami/model/adapters/memory/collection.rb +132 -0
- data/lib/hanami/model/adapters/memory/command.rb +113 -0
- data/lib/hanami/model/adapters/memory/query.rb +653 -0
- data/lib/hanami/model/adapters/memory_adapter.rb +179 -0
- data/lib/hanami/model/adapters/null_adapter.rb +24 -0
- data/lib/hanami/model/adapters/sql/collection.rb +287 -0
- data/lib/hanami/model/adapters/sql/command.rb +73 -0
- data/lib/hanami/model/adapters/sql/console.rb +33 -0
- data/lib/hanami/model/adapters/sql/consoles/mysql.rb +49 -0
- data/lib/hanami/model/adapters/sql/consoles/postgresql.rb +48 -0
- data/lib/hanami/model/adapters/sql/consoles/sqlite.rb +26 -0
- data/lib/hanami/model/adapters/sql/query.rb +788 -0
- data/lib/hanami/model/adapters/sql_adapter.rb +296 -0
- data/lib/hanami/model/coercer.rb +74 -0
- data/lib/hanami/model/config/adapter.rb +116 -0
- data/lib/hanami/model/config/mapper.rb +45 -0
- data/lib/hanami/model/configuration.rb +275 -0
- data/lib/hanami/model/error.rb +7 -0
- data/lib/hanami/model/mapper.rb +124 -0
- data/lib/hanami/model/mapping.rb +48 -0
- data/lib/hanami/model/mapping/attribute.rb +85 -0
- data/lib/hanami/model/mapping/coercers.rb +314 -0
- data/lib/hanami/model/mapping/collection.rb +490 -0
- data/lib/hanami/model/mapping/collection_coercer.rb +79 -0
- data/lib/hanami/model/migrator.rb +324 -0
- data/lib/hanami/model/migrator/adapter.rb +170 -0
- data/lib/hanami/model/migrator/connection.rb +133 -0
- data/lib/hanami/model/migrator/mysql_adapter.rb +72 -0
- data/lib/hanami/model/migrator/postgres_adapter.rb +119 -0
- data/lib/hanami/model/migrator/sqlite_adapter.rb +110 -0
- data/lib/hanami/model/version.rb +4 -1
- data/lib/hanami/repository.rb +872 -0
- metadata +100 -16
- data/.gitignore +0 -9
- data/Gemfile +0 -4
- data/Rakefile +0 -2
- data/bin/console +0 -14
- data/bin/setup +0 -8
@@ -0,0 +1,133 @@
|
|
1
|
+
module Hanami
|
2
|
+
module Model
|
3
|
+
module Migrator
|
4
|
+
# Sequel connection wrapper
|
5
|
+
#
|
6
|
+
# Normalize external adapters interfaces
|
7
|
+
#
|
8
|
+
# @since 0.5.0
|
9
|
+
# @api private
|
10
|
+
class Connection
|
11
|
+
attr_reader :adapter_connection
|
12
|
+
|
13
|
+
def initialize(adapter_connection)
|
14
|
+
@adapter_connection = adapter_connection
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns DB connection host
|
18
|
+
#
|
19
|
+
# Even when adapter doesn't provide it explicitly it tries to parse
|
20
|
+
#
|
21
|
+
# @since 0.5.0
|
22
|
+
# @api private
|
23
|
+
def host
|
24
|
+
@host ||= opts.fetch(:host, parsed_uri.host)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns DB connection port
|
28
|
+
#
|
29
|
+
# Even when adapter doesn't provide it explicitly it tries to parse
|
30
|
+
#
|
31
|
+
# @since 0.5.0
|
32
|
+
# @api private
|
33
|
+
def port
|
34
|
+
@port ||= opts.fetch(:port, parsed_uri.port)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns DB name from conenction
|
38
|
+
#
|
39
|
+
# Even when adapter doesn't provide it explicitly it tries to parse
|
40
|
+
#
|
41
|
+
# @since 0.5.0
|
42
|
+
# @api private
|
43
|
+
def database
|
44
|
+
@database ||= opts.fetch(:database, parsed_uri.path[1..-1])
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns DB type
|
48
|
+
#
|
49
|
+
# @example
|
50
|
+
# connection.database_type
|
51
|
+
# # => 'postgres'
|
52
|
+
#
|
53
|
+
# @since 0.5.0
|
54
|
+
# @api private
|
55
|
+
def database_type
|
56
|
+
adapter_connection.database_type
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns user from DB connection
|
60
|
+
#
|
61
|
+
# Even when adapter doesn't provide it explicitly it tries to parse
|
62
|
+
#
|
63
|
+
# @since 0.5.0
|
64
|
+
# @api private
|
65
|
+
def user
|
66
|
+
@user ||= opts.fetch(:user, parsed_opt('user'))
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns user from DB connection
|
70
|
+
#
|
71
|
+
# Even when adapter doesn't provide it explicitly it tries to parse
|
72
|
+
#
|
73
|
+
# @since 0.5.0
|
74
|
+
# @api private
|
75
|
+
def password
|
76
|
+
@password ||= opts.fetch(:password, parsed_opt('password'))
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns DB connection URI directly from adapter
|
80
|
+
#
|
81
|
+
# @since 0.5.0
|
82
|
+
# @api private
|
83
|
+
def uri
|
84
|
+
adapter_connection.uri
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns DB connection wihout specifying database name
|
88
|
+
#
|
89
|
+
# @since 0.5.0
|
90
|
+
# @api private
|
91
|
+
def global_uri
|
92
|
+
adapter_connection.uri.sub(parsed_uri.select(:path).first, '')
|
93
|
+
end
|
94
|
+
|
95
|
+
# Returns a boolean telling if a DB connection is from JDBC or not
|
96
|
+
#
|
97
|
+
# @since 0.5.0
|
98
|
+
# @api private
|
99
|
+
def jdbc?
|
100
|
+
!adapter_connection.uri.scan('jdbc:').empty?
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns database connection URI instance without JDBC namespace
|
104
|
+
#
|
105
|
+
# @since 0.5.0
|
106
|
+
# @api private
|
107
|
+
def parsed_uri
|
108
|
+
@uri ||= URI.parse(adapter_connection.uri.sub('jdbc:', ''))
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
# Returns a value of a given query string param
|
114
|
+
#
|
115
|
+
# @param option [String] which option from database connection will be extracted from URI
|
116
|
+
#
|
117
|
+
# @since 0.5.0
|
118
|
+
# @api private
|
119
|
+
def parsed_opt(option)
|
120
|
+
parsed_uri.to_s.match(/[\?|\&]#{ option }=(\w+)\&?/).to_a.last
|
121
|
+
end
|
122
|
+
|
123
|
+
# Fetch connection options from adapter
|
124
|
+
#
|
125
|
+
# @since 0.5.0
|
126
|
+
# @api private
|
127
|
+
def opts
|
128
|
+
adapter_connection.opts
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Hanami
|
2
|
+
module Model
|
3
|
+
module Migrator
|
4
|
+
# MySQL adapter
|
5
|
+
#
|
6
|
+
# @since 0.4.0
|
7
|
+
# @api private
|
8
|
+
class MySQLAdapter < Adapter
|
9
|
+
# @since 0.4.0
|
10
|
+
# @api private
|
11
|
+
def create
|
12
|
+
new_connection(global: true).run %(CREATE DATABASE #{ database };)
|
13
|
+
rescue Sequel::DatabaseError => e
|
14
|
+
message = if e.message.match(/database exists/)
|
15
|
+
"Database creation failed. There is 1 other session using the database"
|
16
|
+
else
|
17
|
+
e.message
|
18
|
+
end
|
19
|
+
|
20
|
+
raise MigrationError.new(message)
|
21
|
+
end
|
22
|
+
|
23
|
+
# @since 0.4.0
|
24
|
+
# @api private
|
25
|
+
def drop
|
26
|
+
new_connection(global: true).run %(DROP DATABASE #{ database };)
|
27
|
+
rescue Sequel::DatabaseError => e
|
28
|
+
message = if e.message.match(/doesn\'t exist/)
|
29
|
+
"Cannot find database: #{ database }"
|
30
|
+
else
|
31
|
+
e.message
|
32
|
+
end
|
33
|
+
|
34
|
+
raise MigrationError.new(message)
|
35
|
+
end
|
36
|
+
|
37
|
+
# @since 0.4.0
|
38
|
+
# @api private
|
39
|
+
def dump
|
40
|
+
dump_structure
|
41
|
+
dump_migrations_data
|
42
|
+
end
|
43
|
+
|
44
|
+
# @since 0.4.0
|
45
|
+
# @api private
|
46
|
+
def load
|
47
|
+
load_structure
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# @since 0.4.0
|
53
|
+
# @api private
|
54
|
+
def dump_structure
|
55
|
+
system "mysqldump --user=#{ username } --password=#{ password } --no-data --skip-comments --ignore-table=#{ database }.#{ migrations_table } #{ database } > #{ schema }"
|
56
|
+
end
|
57
|
+
|
58
|
+
# @since 0.4.0
|
59
|
+
# @api private
|
60
|
+
def load_structure
|
61
|
+
system "mysql --user=#{ username } --password=#{ password } #{ database } < #{ escape(schema) }" if schema.exist?
|
62
|
+
end
|
63
|
+
|
64
|
+
# @since 0.4.0
|
65
|
+
# @api private
|
66
|
+
def dump_migrations_data
|
67
|
+
system "mysqldump --user=#{ username } --password=#{ password } --skip-comments #{ database } #{ migrations_table } >> #{ schema }"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module Hanami
|
2
|
+
module Model
|
3
|
+
module Migrator
|
4
|
+
# PostgreSQL adapter
|
5
|
+
#
|
6
|
+
# @since 0.4.0
|
7
|
+
# @api private
|
8
|
+
class PostgresAdapter < Adapter
|
9
|
+
# @since 0.4.0
|
10
|
+
# @api private
|
11
|
+
HOST = 'PGHOST'.freeze
|
12
|
+
|
13
|
+
# @since 0.4.0
|
14
|
+
# @api private
|
15
|
+
PORT = 'PGPORT'.freeze
|
16
|
+
|
17
|
+
# @since 0.4.0
|
18
|
+
# @api private
|
19
|
+
USER = 'PGUSER'.freeze
|
20
|
+
|
21
|
+
# @since 0.4.0
|
22
|
+
# @api private
|
23
|
+
PASSWORD = 'PGPASSWORD'.freeze
|
24
|
+
|
25
|
+
# @since 0.4.0
|
26
|
+
# @api private
|
27
|
+
def create
|
28
|
+
set_environment_variables
|
29
|
+
|
30
|
+
call_db_command('createdb') do |error_message|
|
31
|
+
message = if error_message.match(/already exists/)
|
32
|
+
"createdb: database creation failed. There is 1 other session using the database."
|
33
|
+
else
|
34
|
+
error_message
|
35
|
+
end
|
36
|
+
|
37
|
+
raise MigrationError.new(message)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# @since 0.4.0
|
42
|
+
# @api private
|
43
|
+
def drop
|
44
|
+
set_environment_variables
|
45
|
+
|
46
|
+
call_db_command('dropdb') do |error_message|
|
47
|
+
message = if error_message.match(/does not exist/)
|
48
|
+
"Cannot find database: #{ database }"
|
49
|
+
else
|
50
|
+
error_message
|
51
|
+
end
|
52
|
+
|
53
|
+
raise MigrationError.new(message)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# @since 0.4.0
|
58
|
+
# @api private
|
59
|
+
def dump
|
60
|
+
set_environment_variables
|
61
|
+
dump_structure
|
62
|
+
dump_migrations_data
|
63
|
+
end
|
64
|
+
|
65
|
+
# @since 0.4.0
|
66
|
+
# @api private
|
67
|
+
def load
|
68
|
+
set_environment_variables
|
69
|
+
load_structure
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
# @since 0.4.0
|
75
|
+
# @api private
|
76
|
+
def set_environment_variables
|
77
|
+
ENV[HOST] = host unless host.nil?
|
78
|
+
ENV[PORT] = port.to_s unless port.nil?
|
79
|
+
ENV[PASSWORD] = password unless password.nil?
|
80
|
+
ENV[USER] = username unless username.nil?
|
81
|
+
end
|
82
|
+
|
83
|
+
# @since 0.4.0
|
84
|
+
# @api private
|
85
|
+
def dump_structure
|
86
|
+
system "pg_dump -i -s -x -O -T #{ migrations_table } -f #{ escape(schema) } #{ database }"
|
87
|
+
end
|
88
|
+
|
89
|
+
# @since 0.4.0
|
90
|
+
# @api private
|
91
|
+
def load_structure
|
92
|
+
system "psql -X -q -f #{ escape(schema) } #{ database }" if schema.exist?
|
93
|
+
end
|
94
|
+
|
95
|
+
# @since 0.4.0
|
96
|
+
# @api private
|
97
|
+
def dump_migrations_data
|
98
|
+
system "pg_dump -t #{ migrations_table } #{ database } >> #{ escape(schema) }"
|
99
|
+
end
|
100
|
+
|
101
|
+
# @since 0.5.1
|
102
|
+
# @api private
|
103
|
+
def call_db_command(command)
|
104
|
+
require 'open3'
|
105
|
+
|
106
|
+
begin
|
107
|
+
Open3.popen3(command, database) do |stdin, stdout, stderr, wait_thr|
|
108
|
+
unless wait_thr.value.success? # wait_thr.value is the exit status
|
109
|
+
yield stderr.read
|
110
|
+
end
|
111
|
+
end
|
112
|
+
rescue SystemCallError => e
|
113
|
+
yield e.message
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
module Model
|
5
|
+
module Migrator
|
6
|
+
# SQLite3 Migrator
|
7
|
+
#
|
8
|
+
# @since 0.4.0
|
9
|
+
# @api private
|
10
|
+
class SQLiteAdapter < Adapter
|
11
|
+
# No-op for in-memory databases
|
12
|
+
#
|
13
|
+
# @since 0.4.0
|
14
|
+
# @api private
|
15
|
+
module Memory
|
16
|
+
# @since 0.4.0
|
17
|
+
# @api private
|
18
|
+
def create
|
19
|
+
end
|
20
|
+
|
21
|
+
# @since 0.4.0
|
22
|
+
# @api private
|
23
|
+
def drop
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Initialize adapter
|
28
|
+
#
|
29
|
+
# @since 0.4.0
|
30
|
+
# @api private
|
31
|
+
def initialize(connection)
|
32
|
+
super
|
33
|
+
extend Memory if memory?
|
34
|
+
end
|
35
|
+
|
36
|
+
# @since 0.4.0
|
37
|
+
# @api private
|
38
|
+
def create
|
39
|
+
path.dirname.mkpath
|
40
|
+
FileUtils.touch(path)
|
41
|
+
rescue Errno::EACCES, Errno::EPERM
|
42
|
+
raise MigrationError.new("Permission denied: #{ path.sub(/\A\/\//, '') }")
|
43
|
+
end
|
44
|
+
|
45
|
+
# @since 0.4.0
|
46
|
+
# @api private
|
47
|
+
def drop
|
48
|
+
path.delete
|
49
|
+
rescue Errno::ENOENT
|
50
|
+
raise MigrationError.new("Cannot find database: #{ path.sub(/\A\/\//, '') }")
|
51
|
+
end
|
52
|
+
|
53
|
+
# @since 0.4.0
|
54
|
+
# @api private
|
55
|
+
def dump
|
56
|
+
dump_structure
|
57
|
+
dump_migrations_data
|
58
|
+
end
|
59
|
+
|
60
|
+
# @since 0.4.0
|
61
|
+
# @api private
|
62
|
+
def load
|
63
|
+
load_structure
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
# @since 0.4.0
|
69
|
+
# @api private
|
70
|
+
def path
|
71
|
+
root.join(
|
72
|
+
@connection.uri.sub(/(jdbc\:|)sqlite\:\/\//, '')
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
# @since 0.4.0
|
77
|
+
# @api private
|
78
|
+
def root
|
79
|
+
Hanami::Model.configuration.root
|
80
|
+
end
|
81
|
+
|
82
|
+
# @since 0.4.0
|
83
|
+
# @api private
|
84
|
+
def memory?
|
85
|
+
uri = path.to_s
|
86
|
+
uri.match(/sqlite\:\/\z/) ||
|
87
|
+
uri.match(/\:memory\:/)
|
88
|
+
end
|
89
|
+
|
90
|
+
# @since 0.4.0
|
91
|
+
# @api private
|
92
|
+
def dump_structure
|
93
|
+
system "sqlite3 #{ escape(path) } .schema > #{ escape(schema) }"
|
94
|
+
end
|
95
|
+
|
96
|
+
# @since 0.4.0
|
97
|
+
# @api private
|
98
|
+
def load_structure
|
99
|
+
system "sqlite3 #{ escape(path) } < #{ escape(schema) }" if schema.exist?
|
100
|
+
end
|
101
|
+
|
102
|
+
# @since 0.4.0
|
103
|
+
# @api private
|
104
|
+
def dump_migrations_data
|
105
|
+
system %(sqlite3 #{ escape(path) } .dump | grep '^INSERT INTO "#{ migrations_table }"' >> #{ escape(schema) })
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
data/lib/hanami/model/version.rb
CHANGED
@@ -0,0 +1,872 @@
|
|
1
|
+
require 'hanami/utils/class_attribute'
|
2
|
+
require 'hanami/model/adapters/null_adapter'
|
3
|
+
|
4
|
+
module Hanami
|
5
|
+
# Mediates between the entities and the persistence layer, by offering an API
|
6
|
+
# to query and execute commands on a database.
|
7
|
+
#
|
8
|
+
#
|
9
|
+
#
|
10
|
+
# By default, a repository is named after an entity, by appending the
|
11
|
+
# `Repository` suffix to the entity class name.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# require 'hanami/model'
|
15
|
+
#
|
16
|
+
# class Article
|
17
|
+
# include Hanami::Entity
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# # valid
|
21
|
+
# class ArticleRepository
|
22
|
+
# include Hanami::Repository
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# # not valid for Article
|
26
|
+
# class PostRepository
|
27
|
+
# include Hanami::Repository
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# Repository for an entity can be configured by setting # the `#repository`
|
31
|
+
# on the mapper.
|
32
|
+
#
|
33
|
+
# @example
|
34
|
+
# # PostRepository is repository for Article
|
35
|
+
# mapper = Hanami::Model::Mapper.new do
|
36
|
+
# collection :articles do
|
37
|
+
# entity Article
|
38
|
+
# repository PostRepository
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# A repository is storage independent.
|
43
|
+
# All the queries and commands are delegated to the current adapter.
|
44
|
+
#
|
45
|
+
# This architecture has several advantages:
|
46
|
+
#
|
47
|
+
# * Applications depend on an abstract API, instead of low level details
|
48
|
+
# (Dependency Inversion principle)
|
49
|
+
#
|
50
|
+
# * Applications depend on a stable API, that doesn't change if the
|
51
|
+
# storage changes
|
52
|
+
#
|
53
|
+
# * Developers can postpone storage decisions
|
54
|
+
#
|
55
|
+
# * Isolates the persistence logic at a low level
|
56
|
+
#
|
57
|
+
# Hanami::Model is shipped with two adapters:
|
58
|
+
#
|
59
|
+
# * SqlAdapter
|
60
|
+
# * MemoryAdapter
|
61
|
+
#
|
62
|
+
#
|
63
|
+
#
|
64
|
+
# All the queries and commands are private.
|
65
|
+
# This decision forces developers to define intention revealing API, instead
|
66
|
+
# leak storage API details outside of a repository.
|
67
|
+
#
|
68
|
+
# @example
|
69
|
+
# require 'hanami/model'
|
70
|
+
#
|
71
|
+
# # This is bad for several reasons:
|
72
|
+
# #
|
73
|
+
# # * The caller has an intimate knowledge of the internal mechanisms
|
74
|
+
# # of the Repository.
|
75
|
+
# #
|
76
|
+
# # * The caller works on several levels of abstraction.
|
77
|
+
# #
|
78
|
+
# # * It doesn't express a clear intent, it's just a chain of methods.
|
79
|
+
# #
|
80
|
+
# # * The caller can't be easily tested in isolation.
|
81
|
+
# #
|
82
|
+
# # * If we change the storage, we are forced to change the code of the
|
83
|
+
# # caller(s).
|
84
|
+
#
|
85
|
+
# ArticleRepository.where(author_id: 23).order(:published_at).limit(8)
|
86
|
+
#
|
87
|
+
#
|
88
|
+
#
|
89
|
+
# # This is a huge improvement:
|
90
|
+
# #
|
91
|
+
# # * The caller doesn't know how the repository fetches the entities.
|
92
|
+
# #
|
93
|
+
# # * The caller works on a single level of abstraction.
|
94
|
+
# # It doesn't even know about records, only works with entities.
|
95
|
+
# #
|
96
|
+
# # * It expresses a clear intent.
|
97
|
+
# #
|
98
|
+
# # * The caller can be easily tested in isolation.
|
99
|
+
# # It's just a matter of stub this method.
|
100
|
+
# #
|
101
|
+
# # * If we change the storage, the callers aren't affected.
|
102
|
+
#
|
103
|
+
# ArticleRepository.most_recent_by_author(author)
|
104
|
+
#
|
105
|
+
# class ArticleRepository
|
106
|
+
# include Hanami::Repository
|
107
|
+
#
|
108
|
+
# def self.most_recent_by_author(author, limit = 8)
|
109
|
+
# query do
|
110
|
+
# where(author_id: author.id).
|
111
|
+
# order(:published_at)
|
112
|
+
# end.limit(limit)
|
113
|
+
# end
|
114
|
+
# end
|
115
|
+
#
|
116
|
+
# @since 0.1.0
|
117
|
+
#
|
118
|
+
# @see Hanami::Entity
|
119
|
+
# @see http://martinfowler.com/eaaCatalog/repository.html
|
120
|
+
# @see http://en.wikipedia.org/wiki/Dependency_inversion_principle
|
121
|
+
module Repository
|
122
|
+
# Inject the public API into the hosting class.
|
123
|
+
#
|
124
|
+
# @since 0.1.0
|
125
|
+
#
|
126
|
+
# @example
|
127
|
+
# require 'hanami/model'
|
128
|
+
#
|
129
|
+
# class UserRepository
|
130
|
+
# include Hanami::Repository
|
131
|
+
# end
|
132
|
+
def self.included(base)
|
133
|
+
base.class_eval do
|
134
|
+
extend ClassMethods
|
135
|
+
include Hanami::Utils::ClassAttribute
|
136
|
+
|
137
|
+
class_attribute :collection
|
138
|
+
self.adapter = Hanami::Model::Adapters::NullAdapter.new
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
module ClassMethods
|
143
|
+
# Assigns an adapter.
|
144
|
+
#
|
145
|
+
# Hanami::Model is shipped with two adapters:
|
146
|
+
#
|
147
|
+
# * SqlAdapter
|
148
|
+
# * MemoryAdapter
|
149
|
+
#
|
150
|
+
# @param adapter [Object] an object that implements
|
151
|
+
# `Hanami::Model::Adapters::Abstract` interface
|
152
|
+
#
|
153
|
+
# @since 0.1.0
|
154
|
+
#
|
155
|
+
# @see Hanami::Model::Adapters::SqlAdapter
|
156
|
+
# @see Hanami::Model::Adapters::MemoryAdapter
|
157
|
+
#
|
158
|
+
# @example Memory adapter
|
159
|
+
# require 'hanami/model'
|
160
|
+
# require 'hanami/model/adapters/memory_adapter'
|
161
|
+
#
|
162
|
+
# mapper = Hanami::Model::Mapper.new do
|
163
|
+
# # ...
|
164
|
+
# end
|
165
|
+
#
|
166
|
+
# adapter = Hanami::Model::Adapters::MemoryAdapter.new(mapper)
|
167
|
+
#
|
168
|
+
# class UserRepository
|
169
|
+
# include Hanami::Repository
|
170
|
+
# end
|
171
|
+
#
|
172
|
+
# UserRepository.adapter = adapter
|
173
|
+
#
|
174
|
+
#
|
175
|
+
#
|
176
|
+
# @example SQL adapter with a Sqlite database
|
177
|
+
# require 'sqlite3'
|
178
|
+
# require 'hanami/model'
|
179
|
+
# require 'hanami/model/adapters/sql_adapter'
|
180
|
+
#
|
181
|
+
# mapper = Hanami::Model::Mapper.new do
|
182
|
+
# # ...
|
183
|
+
# end
|
184
|
+
#
|
185
|
+
# adapter = Hanami::Model::Adapters::SqlAdapter.new(mapper, 'sqlite://path/to/database.db')
|
186
|
+
#
|
187
|
+
# class UserRepository
|
188
|
+
# include Hanami::Repository
|
189
|
+
# end
|
190
|
+
#
|
191
|
+
# UserRepository.adapter = adapter
|
192
|
+
#
|
193
|
+
#
|
194
|
+
#
|
195
|
+
# @example SQL adapter with a Postgres database
|
196
|
+
# require 'pg'
|
197
|
+
# require 'hanami/model'
|
198
|
+
# require 'hanami/model/adapters/sql_adapter'
|
199
|
+
#
|
200
|
+
# mapper = Hanami::Model::Mapper.new do
|
201
|
+
# # ...
|
202
|
+
# end
|
203
|
+
#
|
204
|
+
# adapter = Hanami::Model::Adapters::SqlAdapter.new(mapper, 'postgres://host:port/database')
|
205
|
+
#
|
206
|
+
# class UserRepository
|
207
|
+
# include Hanami::Repository
|
208
|
+
# end
|
209
|
+
#
|
210
|
+
# UserRepository.adapter = adapter
|
211
|
+
def adapter=(adapter)
|
212
|
+
@adapter = adapter
|
213
|
+
end
|
214
|
+
|
215
|
+
# @since 0.5.0
|
216
|
+
# @api private
|
217
|
+
def adapter
|
218
|
+
@adapter
|
219
|
+
end
|
220
|
+
|
221
|
+
# Creates or updates a record in the database for the given entity.
|
222
|
+
#
|
223
|
+
# @param entity [#id, #id=] the entity to persist
|
224
|
+
#
|
225
|
+
# @return [Object] a copy of the entity with `id` assigned
|
226
|
+
#
|
227
|
+
# @since 0.1.0
|
228
|
+
#
|
229
|
+
# @see Hanami::Repository#create
|
230
|
+
# @see Hanami::Repository#update
|
231
|
+
#
|
232
|
+
# @example With a non persisted entity
|
233
|
+
# require 'hanami/model'
|
234
|
+
#
|
235
|
+
# class ArticleRepository
|
236
|
+
# include Hanami::Repository
|
237
|
+
# end
|
238
|
+
#
|
239
|
+
# article = Article.new(title: 'Introducing Hanami::Model')
|
240
|
+
# article.id # => nil
|
241
|
+
#
|
242
|
+
# persisted_article = ArticleRepository.persist(article) # creates a record
|
243
|
+
# article.id # => nil
|
244
|
+
# persisted_article.id # => 23
|
245
|
+
#
|
246
|
+
# @example With a persisted entity
|
247
|
+
# require 'hanami/model'
|
248
|
+
#
|
249
|
+
# class ArticleRepository
|
250
|
+
# include Hanami::Repository
|
251
|
+
# end
|
252
|
+
#
|
253
|
+
# article = ArticleRepository.find(23)
|
254
|
+
# article.id # => 23
|
255
|
+
#
|
256
|
+
# article.title = 'Launching Hanami::Model'
|
257
|
+
# ArticleRepository.persist(article) # updates the record
|
258
|
+
#
|
259
|
+
# article = ArticleRepository.find(23)
|
260
|
+
# article.title # => "Launching Hanami::Model"
|
261
|
+
def persist(entity)
|
262
|
+
_touch(entity)
|
263
|
+
@adapter.persist(collection, entity)
|
264
|
+
end
|
265
|
+
|
266
|
+
# Creates a record in the database for the given entity.
|
267
|
+
# It returns a copy of the entity with `id` assigned.
|
268
|
+
#
|
269
|
+
# If already persisted (`id` present) it does nothing.
|
270
|
+
#
|
271
|
+
# @param entity [#id,#id=] the entity to create
|
272
|
+
#
|
273
|
+
# @return [Object] a copy of the entity with `id` assigned
|
274
|
+
#
|
275
|
+
# @since 0.1.0
|
276
|
+
#
|
277
|
+
# @see Hanami::Repository#persist
|
278
|
+
#
|
279
|
+
# @example
|
280
|
+
# require 'hanami/model'
|
281
|
+
#
|
282
|
+
# class ArticleRepository
|
283
|
+
# include Hanami::Repository
|
284
|
+
# end
|
285
|
+
#
|
286
|
+
# article = Article.new(title: 'Introducing Hanami::Model')
|
287
|
+
# article.id # => nil
|
288
|
+
#
|
289
|
+
# created_article = ArticleRepository.create(article) # creates a record
|
290
|
+
# article.id # => nil
|
291
|
+
# created_article.id # => 23
|
292
|
+
#
|
293
|
+
# created_article = ArticleRepository.create(article)
|
294
|
+
# created_article.id # => 24
|
295
|
+
#
|
296
|
+
# created_article = ArticleRepository.create(existing_article) # => no-op
|
297
|
+
# created_article # => nil
|
298
|
+
#
|
299
|
+
def create(entity)
|
300
|
+
unless _persisted?(entity)
|
301
|
+
_touch(entity)
|
302
|
+
@adapter.create(collection, entity)
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
# Updates a record in the database corresponding to the given entity.
|
307
|
+
#
|
308
|
+
# If not already persisted (`id` present) it raises an exception.
|
309
|
+
#
|
310
|
+
# @param entity [#id] the entity to update
|
311
|
+
#
|
312
|
+
# @return [Object] the entity
|
313
|
+
#
|
314
|
+
# @raise [Hanami::Model::NonPersistedEntityError] if the given entity
|
315
|
+
# wasn't already persisted.
|
316
|
+
#
|
317
|
+
# @since 0.1.0
|
318
|
+
#
|
319
|
+
# @see Hanami::Repository#persist
|
320
|
+
# @see Hanami::Model::NonPersistedEntityError
|
321
|
+
#
|
322
|
+
# @example With a persisted entity
|
323
|
+
# require 'hanami/model'
|
324
|
+
#
|
325
|
+
# class ArticleRepository
|
326
|
+
# include Hanami::Repository
|
327
|
+
# end
|
328
|
+
#
|
329
|
+
# article = ArticleRepository.find(23)
|
330
|
+
# article.id # => 23
|
331
|
+
# article.title = 'Launching Hanami::Model'
|
332
|
+
#
|
333
|
+
# ArticleRepository.update(article) # updates the record
|
334
|
+
#
|
335
|
+
#
|
336
|
+
#
|
337
|
+
# @example With a non persisted entity
|
338
|
+
# require 'hanami/model'
|
339
|
+
#
|
340
|
+
# class ArticleRepository
|
341
|
+
# include Hanami::Repository
|
342
|
+
# end
|
343
|
+
#
|
344
|
+
# article = Article.new(title: 'Introducing Hanami::Model')
|
345
|
+
# article.id # => nil
|
346
|
+
#
|
347
|
+
# ArticleRepository.update(article) # raises Hanami::Model::NonPersistedEntityError
|
348
|
+
def update(entity)
|
349
|
+
if _persisted?(entity)
|
350
|
+
_touch(entity)
|
351
|
+
@adapter.update(collection, entity)
|
352
|
+
else
|
353
|
+
raise Hanami::Model::NonPersistedEntityError
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
# Deletes a record in the database corresponding to the given entity.
|
358
|
+
#
|
359
|
+
# If not already persisted (`id` present) it raises an exception.
|
360
|
+
#
|
361
|
+
# @param entity [#id] the entity to delete
|
362
|
+
#
|
363
|
+
# @return [Object] the entity
|
364
|
+
#
|
365
|
+
# @raise [Hanami::Model::NonPersistedEntityError] if the given entity
|
366
|
+
# wasn't already persisted.
|
367
|
+
#
|
368
|
+
# @since 0.1.0
|
369
|
+
#
|
370
|
+
# @see Hanami::Model::NonPersistedEntityError
|
371
|
+
#
|
372
|
+
# @example With a persisted entity
|
373
|
+
# require 'hanami/model'
|
374
|
+
#
|
375
|
+
# class ArticleRepository
|
376
|
+
# include Hanami::Repository
|
377
|
+
# end
|
378
|
+
#
|
379
|
+
# article = ArticleRepository.find(23)
|
380
|
+
# article.id # => 23
|
381
|
+
#
|
382
|
+
# ArticleRepository.delete(article) # deletes the record
|
383
|
+
#
|
384
|
+
#
|
385
|
+
#
|
386
|
+
# @example With a non persisted entity
|
387
|
+
# require 'hanami/model'
|
388
|
+
#
|
389
|
+
# class ArticleRepository
|
390
|
+
# include Hanami::Repository
|
391
|
+
# end
|
392
|
+
#
|
393
|
+
# article = Article.new(title: 'Introducing Hanami::Model')
|
394
|
+
# article.id # => nil
|
395
|
+
#
|
396
|
+
# ArticleRepository.delete(article) # raises Hanami::Model::NonPersistedEntityError
|
397
|
+
def delete(entity)
|
398
|
+
if _persisted?(entity)
|
399
|
+
@adapter.delete(collection, entity)
|
400
|
+
else
|
401
|
+
raise Hanami::Model::NonPersistedEntityError
|
402
|
+
end
|
403
|
+
|
404
|
+
entity
|
405
|
+
end
|
406
|
+
|
407
|
+
# Returns all the persisted entities.
|
408
|
+
#
|
409
|
+
# @return [Array<Object>] the result of the query
|
410
|
+
#
|
411
|
+
# @since 0.1.0
|
412
|
+
#
|
413
|
+
# @example
|
414
|
+
# require 'hanami/model'
|
415
|
+
#
|
416
|
+
# class ArticleRepository
|
417
|
+
# include Hanami::Repository
|
418
|
+
# end
|
419
|
+
#
|
420
|
+
# ArticleRepository.all # => [ #<Article:0x007f9b19a60098> ]
|
421
|
+
def all
|
422
|
+
@adapter.all(collection)
|
423
|
+
end
|
424
|
+
|
425
|
+
# Finds an entity by its identity.
|
426
|
+
#
|
427
|
+
# If used with a SQL database, it corresponds to the primary key.
|
428
|
+
#
|
429
|
+
# @param id [Object] the identity of the entity
|
430
|
+
#
|
431
|
+
# @return [Object,NilClass] the result of the query, if present
|
432
|
+
#
|
433
|
+
# @since 0.1.0
|
434
|
+
#
|
435
|
+
# @example
|
436
|
+
# require 'hanami/model'
|
437
|
+
#
|
438
|
+
# class ArticleRepository
|
439
|
+
# include Hanami::Repository
|
440
|
+
# end
|
441
|
+
#
|
442
|
+
# ArticleRepository.find(23) # => #<Article:0x007f9b19a60098>
|
443
|
+
# ArticleRepository.find(9999) # => nil
|
444
|
+
def find(id)
|
445
|
+
@adapter.find(collection, id)
|
446
|
+
end
|
447
|
+
|
448
|
+
# Returns the first entity in the database.
|
449
|
+
#
|
450
|
+
# @return [Object,nil] the result of the query
|
451
|
+
#
|
452
|
+
# @since 0.1.0
|
453
|
+
#
|
454
|
+
# @see Hanami::Repository#last
|
455
|
+
#
|
456
|
+
# @example With at least one persisted entity
|
457
|
+
# require 'hanami/model'
|
458
|
+
#
|
459
|
+
# class ArticleRepository
|
460
|
+
# include Hanami::Repository
|
461
|
+
# end
|
462
|
+
#
|
463
|
+
# ArticleRepository.first # => #<Article:0x007f8c71d98a28>
|
464
|
+
#
|
465
|
+
# @example With an empty collection
|
466
|
+
# require 'hanami/model'
|
467
|
+
#
|
468
|
+
# class ArticleRepository
|
469
|
+
# include Hanami::Repository
|
470
|
+
# end
|
471
|
+
#
|
472
|
+
# ArticleRepository.first # => nil
|
473
|
+
def first
|
474
|
+
@adapter.first(collection)
|
475
|
+
end
|
476
|
+
|
477
|
+
# Returns the last entity in the database.
|
478
|
+
#
|
479
|
+
# @return [Object,nil] the result of the query
|
480
|
+
#
|
481
|
+
# @since 0.1.0
|
482
|
+
#
|
483
|
+
# @see Hanami::Repository#last
|
484
|
+
#
|
485
|
+
# @example With at least one persisted entity
|
486
|
+
# require 'hanami/model'
|
487
|
+
#
|
488
|
+
# class ArticleRepository
|
489
|
+
# include Hanami::Repository
|
490
|
+
# end
|
491
|
+
#
|
492
|
+
# ArticleRepository.last # => #<Article:0x007f8c71d98a28>
|
493
|
+
#
|
494
|
+
# @example With an empty collection
|
495
|
+
# require 'hanami/model'
|
496
|
+
#
|
497
|
+
# class ArticleRepository
|
498
|
+
# include Hanami::Repository
|
499
|
+
# end
|
500
|
+
#
|
501
|
+
# ArticleRepository.last # => nil
|
502
|
+
def last
|
503
|
+
@adapter.last(collection)
|
504
|
+
end
|
505
|
+
|
506
|
+
# Deletes all the records from the current collection.
|
507
|
+
#
|
508
|
+
# If used with a SQL database it executes a `DELETE FROM <table>`.
|
509
|
+
#
|
510
|
+
# @since 0.1.0
|
511
|
+
#
|
512
|
+
# @example
|
513
|
+
# require 'hanami/model'
|
514
|
+
#
|
515
|
+
# class ArticleRepository
|
516
|
+
# include Hanami::Repository
|
517
|
+
# end
|
518
|
+
#
|
519
|
+
# ArticleRepository.clear # deletes all the records
|
520
|
+
def clear
|
521
|
+
@adapter.clear(collection)
|
522
|
+
end
|
523
|
+
|
524
|
+
# Wraps the given block in a transaction.
|
525
|
+
#
|
526
|
+
# For performance reasons the block isn't in the signature of the method,
|
527
|
+
# but it's yielded at the lower level.
|
528
|
+
#
|
529
|
+
# Please note that it's only supported by some databases.
|
530
|
+
# For this reason, the accepted options may be different from adapter to
|
531
|
+
# adapter.
|
532
|
+
#
|
533
|
+
# For advanced scenarios, please check the documentation of each adapter.
|
534
|
+
#
|
535
|
+
# @param options [Hash] options for transaction
|
536
|
+
#
|
537
|
+
# @see Hanami::Model::Adapters::SqlAdapter#transaction
|
538
|
+
# @see Hanami::Model::Adapters::MemoryAdapter#transaction
|
539
|
+
#
|
540
|
+
# @since 0.2.3
|
541
|
+
#
|
542
|
+
# @example Basic usage with SQL adapter
|
543
|
+
# require 'hanami/model'
|
544
|
+
#
|
545
|
+
# class Article
|
546
|
+
# include Hanami::Entity
|
547
|
+
# attributes :title, :body
|
548
|
+
# end
|
549
|
+
#
|
550
|
+
# class ArticleRepository
|
551
|
+
# include Hanami::Repository
|
552
|
+
# end
|
553
|
+
#
|
554
|
+
# article = Article.new(title: 'Introducing transactions',
|
555
|
+
# body: 'lorem ipsum')
|
556
|
+
#
|
557
|
+
# ArticleRepository.transaction do
|
558
|
+
# ArticleRepository.dangerous_operation!(article) # => RuntimeError
|
559
|
+
# # !!! ROLLBACK !!!
|
560
|
+
# end
|
561
|
+
def transaction(options = {})
|
562
|
+
@adapter.transaction(options) do
|
563
|
+
yield
|
564
|
+
end
|
565
|
+
end
|
566
|
+
|
567
|
+
private
|
568
|
+
|
569
|
+
# Executes the given raw statement on the adapter.
|
570
|
+
#
|
571
|
+
# Please note that it's only supported by some databases,
|
572
|
+
# a `NotImplementedError` will be raised when the adapter does not
|
573
|
+
# responds to the `execute` method.
|
574
|
+
#
|
575
|
+
# For advanced scenarios, please check the documentation of each adapter.
|
576
|
+
#
|
577
|
+
# @param raw [String] the raw statement to execute on the connection
|
578
|
+
#
|
579
|
+
# @return [NilClass]
|
580
|
+
#
|
581
|
+
# @raise [NotImplementedError] if current Hanami::Model adapter doesn't
|
582
|
+
# implement `execute`.
|
583
|
+
#
|
584
|
+
# @raise [Hanami::Model::InvalidCommandError] if the raw statement is invalid
|
585
|
+
#
|
586
|
+
# @see Hanami::Model::Adapters::Abstract#execute
|
587
|
+
# @see Hanami::Model::Adapters::SqlAdapter#execute
|
588
|
+
#
|
589
|
+
# @since 0.3.1
|
590
|
+
#
|
591
|
+
# @example Basic usage with SQL adapter
|
592
|
+
# require 'hanami/model'
|
593
|
+
#
|
594
|
+
# class Article
|
595
|
+
# include Hanami::Entity
|
596
|
+
# attributes :title, :body
|
597
|
+
# end
|
598
|
+
#
|
599
|
+
# class ArticleRepository
|
600
|
+
# include Hanami::Repository
|
601
|
+
#
|
602
|
+
# def self.reset_comments_count
|
603
|
+
# execute "UPDATE articles SET comments_count = 0"
|
604
|
+
# end
|
605
|
+
# end
|
606
|
+
#
|
607
|
+
# ArticleRepository.reset_comments_count
|
608
|
+
def execute(raw)
|
609
|
+
@adapter.execute(raw)
|
610
|
+
end
|
611
|
+
|
612
|
+
# Fetch raw result sets for the the given statement.
|
613
|
+
#
|
614
|
+
# PLEASE NOTE: The returned result set contains an array of hashes.
|
615
|
+
# The columns are returned as they are from the database,
|
616
|
+
# the mapper is bypassed here.
|
617
|
+
#
|
618
|
+
# @param raw [String] the raw statement used to fetch records
|
619
|
+
# @param blk [Proc] an optional block that is yielded for each record
|
620
|
+
#
|
621
|
+
# @return [Enumerable<Hash>,Array<Hash>] the collection of raw records
|
622
|
+
#
|
623
|
+
# @raise [NotImplementedError] if current Hanami::Model adapter doesn't
|
624
|
+
# implement `fetch`.
|
625
|
+
#
|
626
|
+
# @raise [Hanami::Model::InvalidQueryError] if the raw statement is invalid
|
627
|
+
#
|
628
|
+
# @since 0.5.0
|
629
|
+
#
|
630
|
+
# @example Basic Usage
|
631
|
+
# require 'hanami/model'
|
632
|
+
#
|
633
|
+
# mapping do
|
634
|
+
# collection :articles do
|
635
|
+
# attribute :id, Integer, as: :s_id
|
636
|
+
# attribute :title, String, as: :s_title
|
637
|
+
# end
|
638
|
+
# end
|
639
|
+
#
|
640
|
+
# class Article
|
641
|
+
# include Hanami::Entity
|
642
|
+
# attributes :title, :body
|
643
|
+
# end
|
644
|
+
#
|
645
|
+
# class ArticleRepository
|
646
|
+
# include Hanami::Repository
|
647
|
+
#
|
648
|
+
# def self.all_raw
|
649
|
+
# fetch("SELECT * FROM articles")
|
650
|
+
# end
|
651
|
+
# end
|
652
|
+
#
|
653
|
+
# ArticleRepository.all_raw
|
654
|
+
# # => [{:_id=>1, :user_id=>nil, :s_title=>"Art 1", :comments_count=>nil, :umapped_column=>nil}]
|
655
|
+
#
|
656
|
+
# @example Map A Value From Result Set
|
657
|
+
# require 'hanami/model'
|
658
|
+
#
|
659
|
+
# mapping do
|
660
|
+
# collection :articles do
|
661
|
+
# attribute :id, Integer, as: :s_id
|
662
|
+
# attribute :title, String, as: :s_title
|
663
|
+
# end
|
664
|
+
# end
|
665
|
+
#
|
666
|
+
# class Article
|
667
|
+
# include Hanami::Entity
|
668
|
+
# attributes :title, :body
|
669
|
+
# end
|
670
|
+
#
|
671
|
+
# class ArticleRepository
|
672
|
+
# include Hanami::Repository
|
673
|
+
#
|
674
|
+
# def self.titles
|
675
|
+
# fetch("SELECT s_title FROM articles").map do |article|
|
676
|
+
# article[:s_title]
|
677
|
+
# end
|
678
|
+
# end
|
679
|
+
# end
|
680
|
+
#
|
681
|
+
# ArticleRepository.titles # => ["Announcing Hanami v0.5.0"]
|
682
|
+
#
|
683
|
+
# @example Passing A Block
|
684
|
+
# require 'hanami/model'
|
685
|
+
#
|
686
|
+
# mapping do
|
687
|
+
# collection :articles do
|
688
|
+
# attribute :id, Integer, as: :s_id
|
689
|
+
# attribute :title, String, as: :s_title
|
690
|
+
# end
|
691
|
+
# end
|
692
|
+
#
|
693
|
+
# class Article
|
694
|
+
# include Hanami::Entity
|
695
|
+
# attributes :title, :body
|
696
|
+
# end
|
697
|
+
#
|
698
|
+
# class ArticleRepository
|
699
|
+
# include Hanami::Repository
|
700
|
+
#
|
701
|
+
# def self.titles
|
702
|
+
# result = []
|
703
|
+
#
|
704
|
+
# fetch("SELECT s_title FROM articles") do |article|
|
705
|
+
# result << article[:s_title]
|
706
|
+
# end
|
707
|
+
#
|
708
|
+
# result
|
709
|
+
# end
|
710
|
+
# end
|
711
|
+
#
|
712
|
+
# ArticleRepository.titles # => ["Announcing Hanami v0.5.0"]
|
713
|
+
def fetch(raw, &blk)
|
714
|
+
@adapter.fetch(raw, &blk)
|
715
|
+
end
|
716
|
+
|
717
|
+
# Fabricates a query and yields the given block to access the low level
|
718
|
+
# APIs exposed by the query itself.
|
719
|
+
#
|
720
|
+
# This is a Ruby private method, because we wanted to prevent outside
|
721
|
+
# objects to query directly the database. However, this is a public API
|
722
|
+
# method, and this is the only way to filter entities.
|
723
|
+
#
|
724
|
+
# The returned query SHOULD be lazy: the entities should be fetched by
|
725
|
+
# the database only when needed.
|
726
|
+
#
|
727
|
+
# The returned query SHOULD refer to the entire collection by default.
|
728
|
+
#
|
729
|
+
# Queries can be reused and combined together. See the example below.
|
730
|
+
# IMPORTANT: This feature works only with the Sql adapter.
|
731
|
+
#
|
732
|
+
# A repository is storage independent.
|
733
|
+
# All the queries are delegated to the current adapter, which is
|
734
|
+
# responsible to implement a querying API.
|
735
|
+
#
|
736
|
+
# Hanami::Model is shipped with two adapters:
|
737
|
+
#
|
738
|
+
# * SqlAdapter, which yields a Hanami::Model::Adapters::Sql::Query
|
739
|
+
# * MemoryAdapter, which yields a Hanami::Model::Adapters::Memory::Query
|
740
|
+
#
|
741
|
+
# @param blk [Proc] a block of code that is executed in the context of a
|
742
|
+
# query
|
743
|
+
#
|
744
|
+
# @return a query, the type depends on the current adapter
|
745
|
+
#
|
746
|
+
# @api public
|
747
|
+
# @since 0.1.0
|
748
|
+
#
|
749
|
+
# @see Hanami::Model::Adapters::Sql::Query
|
750
|
+
# @see Hanami::Model::Adapters::Memory::Query
|
751
|
+
#
|
752
|
+
# @example
|
753
|
+
# require 'hanami/model'
|
754
|
+
#
|
755
|
+
# class ArticleRepository
|
756
|
+
# include Hanami::Repository
|
757
|
+
#
|
758
|
+
# def self.most_recent_by_author(author, limit = 8)
|
759
|
+
# query do
|
760
|
+
# where(author_id: author.id).
|
761
|
+
# desc(:published_at).
|
762
|
+
# limit(limit)
|
763
|
+
# end
|
764
|
+
# end
|
765
|
+
#
|
766
|
+
# def self.most_recent_published_by_author(author, limit = 8)
|
767
|
+
# # combine .most_recent_published_by_author and .published queries
|
768
|
+
# most_recent_by_author(author, limit).published
|
769
|
+
# end
|
770
|
+
#
|
771
|
+
# def self.published
|
772
|
+
# query do
|
773
|
+
# where(published: true)
|
774
|
+
# end
|
775
|
+
# end
|
776
|
+
#
|
777
|
+
# def self.rank
|
778
|
+
# # reuse .published, which returns a query that respond to #desc
|
779
|
+
# published.desc(:comments_count)
|
780
|
+
# end
|
781
|
+
#
|
782
|
+
# def self.best_article_ever
|
783
|
+
# # reuse .published, which returns a query that respond to #limit
|
784
|
+
# rank.limit(1)
|
785
|
+
# end
|
786
|
+
#
|
787
|
+
# def self.comments_average
|
788
|
+
# query.average(:comments_count)
|
789
|
+
# end
|
790
|
+
# end
|
791
|
+
def query(&blk)
|
792
|
+
@adapter.query(collection, self, &blk)
|
793
|
+
end
|
794
|
+
|
795
|
+
# Negates the filtering conditions of a given query with the logical
|
796
|
+
# opposite operator.
|
797
|
+
#
|
798
|
+
# This is only supported by the SqlAdapter.
|
799
|
+
#
|
800
|
+
# @param query [Object] a query
|
801
|
+
#
|
802
|
+
# @return a negated query, the type depends on the current adapter
|
803
|
+
#
|
804
|
+
# @api public
|
805
|
+
# @since 0.1.0
|
806
|
+
#
|
807
|
+
# @see Hanami::Model::Adapters::Sql::Query#negate!
|
808
|
+
#
|
809
|
+
# @example
|
810
|
+
# require 'hanami/model'
|
811
|
+
#
|
812
|
+
# class ProjectRepository
|
813
|
+
# include Hanami::Repository
|
814
|
+
#
|
815
|
+
# def self.cool
|
816
|
+
# query do
|
817
|
+
# where(language: 'ruby')
|
818
|
+
# end
|
819
|
+
# end
|
820
|
+
#
|
821
|
+
# def self.not_cool
|
822
|
+
# exclude cool
|
823
|
+
# end
|
824
|
+
# end
|
825
|
+
def exclude(query)
|
826
|
+
query.negate!
|
827
|
+
query
|
828
|
+
end
|
829
|
+
|
830
|
+
# This is a method to check entity persited or not
|
831
|
+
#
|
832
|
+
# @param entity
|
833
|
+
# @return a boolean value
|
834
|
+
# @since 0.3.1
|
835
|
+
def _persisted?(entity)
|
836
|
+
!!entity.id
|
837
|
+
end
|
838
|
+
|
839
|
+
# Update timestamps
|
840
|
+
#
|
841
|
+
# @param entity [Object, Hanami::Entity] the entity
|
842
|
+
#
|
843
|
+
# @api private
|
844
|
+
# @since 0.3.1
|
845
|
+
def _touch(entity)
|
846
|
+
now = Time.now.utc
|
847
|
+
|
848
|
+
if _has_timestamp?(entity, :created_at)
|
849
|
+
entity.created_at ||= now
|
850
|
+
end
|
851
|
+
|
852
|
+
if _has_timestamp?(entity, :updated_at)
|
853
|
+
entity.updated_at = now
|
854
|
+
end
|
855
|
+
end
|
856
|
+
|
857
|
+
# Check if the given entity has the given timestamp
|
858
|
+
#
|
859
|
+
# @param entity [Object, Hanami::Entity] the entity
|
860
|
+
# @param timestamp [Symbol] the timestamp name
|
861
|
+
#
|
862
|
+
# @return [TrueClass,FalseClass]
|
863
|
+
#
|
864
|
+
# @api private
|
865
|
+
# @since 0.3.1
|
866
|
+
def _has_timestamp?(entity, timestamp)
|
867
|
+
entity.respond_to?(timestamp) &&
|
868
|
+
entity.respond_to?("#{ timestamp }=")
|
869
|
+
end
|
870
|
+
end
|
871
|
+
end
|
872
|
+
end
|