preserves 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +23 -0
- data/.irbrc +9 -0
- data/.rspec +3 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +178 -0
- data/Rakefile +2 -0
- data/SETUP.sh +15 -0
- data/TODO.md +122 -0
- data/lib/preserves.rb +24 -0
- data/lib/preserves/mapper.rb +108 -0
- data/lib/preserves/mapper/belongs_to.rb +25 -0
- data/lib/preserves/mapper/has_many.rb +25 -0
- data/lib/preserves/mapper/relation.rb +25 -0
- data/lib/preserves/mapping.rb +73 -0
- data/lib/preserves/repository.rb +69 -0
- data/lib/preserves/selection.rb +65 -0
- data/lib/preserves/sql.rb +12 -0
- data/lib/preserves/sql/result_set.rb +21 -0
- data/lib/preserves/version.rb +3 -0
- data/preserves.gemspec +27 -0
- data/spec/repository_spec.rb +260 -0
- data/spec/spec_helper.rb +120 -0
- metadata +140 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f8431d69f3de10e3ee28a0e6e2c96b86afadfeee
|
4
|
+
data.tar.gz: 6c87236337e01dbb1c5e95a95920fc82ef8f7f18
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3e693759454aea1c88303fd6ed6b5e916991ad1491b3217ee9f1558b1c99b094c1be2eed26db95e26ab25cf82d7734829fa3292b4f531626071b1a5c5930aac7
|
7
|
+
data.tar.gz: 25d5b244aebae726bc06607b192c094672e5b96199501a9bb6f915d0bb21c2de5aa27d226f1efb9dbace3445f4fe78ad8ba86060ea2babecc384f80d2f87ef43
|
data/.gitignore
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
23
|
+
vendor/bundle
|
data/.irbrc
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# NOTE: For this file to work, your ~/.irbrc file must contain snippet from http://www.samuelmullen.com/2010/04/irb-global-local-irbrc/.
|
2
|
+
|
3
|
+
|
4
|
+
# Allow reloading our gem.
|
5
|
+
def reload!
|
6
|
+
@gem_name = Dir["#{Dir.pwd}/*.gemspec"].first.split('/').last.sub('.gemspec', '')
|
7
|
+
files = $LOADED_FEATURES.select { |feat| feat =~ %r[/#{@gem_name}/] }
|
8
|
+
files.each { |file| load file }
|
9
|
+
end
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2014 BoochTek, LLC
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
Preserves
|
2
|
+
=========
|
3
|
+
|
4
|
+
Preserves is a minimalist ORM (object-relational mapper) for Ruby, using the
|
5
|
+
Repository and Data Mapper patterns.
|
6
|
+
|
7
|
+
We're trying to answer these questions:
|
8
|
+
|
9
|
+
* How simple can we make an ORM that is still useful?
|
10
|
+
* Developers have to know SQL anyway, so why try to hide the SQL from them?
|
11
|
+
* Is the complexity of a typical ORM really better than the complexity of SQL?
|
12
|
+
* ORMs are a leaky abstraction. What if we made it so leaky that it doesn't matter?
|
13
|
+
|
14
|
+
This ORM is based on a few strong opinions:
|
15
|
+
|
16
|
+
* The Data Mapper pattern is generally better than the Active Record pattern.
|
17
|
+
* Unless you're just writing a CRUD front-end, with little interesting behavior.
|
18
|
+
* Declaring attributes in the domain model is better than hiding them elsewhere.
|
19
|
+
* Declaring relationships in one place and attributes in another is true madness.
|
20
|
+
* NoSQL as a main data store is usually misguided.
|
21
|
+
* PostgreSQL can do just about anything you need, using SQL.
|
22
|
+
* Projects are unlikely to need to abstract SQL to allow them to use different RDBMSes.
|
23
|
+
* Developer workstations are fast enough to run "full" RDBMSes.
|
24
|
+
* If you're not using "interesting" features, then you're probably using "standard" SQL.
|
25
|
+
|
26
|
+
The Data Mapper pattern provides several advantages:
|
27
|
+
|
28
|
+
* Domain objects don't have to know anything about the database or its schema.
|
29
|
+
* Instead, the mapper knows about the domain objects and the database.
|
30
|
+
* DB schema can change without having to change to domain objects; only the mapper changes.
|
31
|
+
* The domain objects are self-contained.
|
32
|
+
* Don't have to look elsewhere to understand everything a class contains.
|
33
|
+
* Better meets the Single Responsibility Principle (SRP).
|
34
|
+
* Domain model classes handle business logic.
|
35
|
+
* Repository classes handle persistence.
|
36
|
+
* Mapper classes handle mapping database fields to object attributes.
|
37
|
+
|
38
|
+
It's been pointed out that Preserves might not in fact even be an ORM, because it doesn't have a complete model of the relations between objects.
|
39
|
+
|
40
|
+
|
41
|
+
Installation
|
42
|
+
------------
|
43
|
+
|
44
|
+
Add this line to your application's Gemfile:
|
45
|
+
|
46
|
+
gem 'preserves'
|
47
|
+
|
48
|
+
And then execute:
|
49
|
+
|
50
|
+
$ bundle
|
51
|
+
|
52
|
+
Or install it yourself as:
|
53
|
+
|
54
|
+
$ gem install preserves
|
55
|
+
|
56
|
+
|
57
|
+
Example Usage
|
58
|
+
-------------
|
59
|
+
|
60
|
+
First, create your domain model class. You can use a [Struct], an
|
61
|
+
[OpenStruct], a [Virtus] model, or a plain old Ruby object (PORO) class.
|
62
|
+
We'll use a Struct in the examples, so we can initialize the fields easily.
|
63
|
+
|
64
|
+
~~~ ruby
|
65
|
+
User = Struct.new(:id, :name, :age) do
|
66
|
+
end
|
67
|
+
~~~
|
68
|
+
|
69
|
+
Next, configure the Preserves data store.
|
70
|
+
|
71
|
+
~~~ ruby
|
72
|
+
Preserves.data_store = Preserves::PostgreSQL("my_database")
|
73
|
+
~~~
|
74
|
+
|
75
|
+
Then create a repository linked to the domain model class.
|
76
|
+
By default, all attributes will be assumed to be Strings.
|
77
|
+
For other attribute types, you'll need to supply the mapping.
|
78
|
+
(We'll have some default mappings determined from the DB or model later.)
|
79
|
+
Your repository should then define methods to access model objects
|
80
|
+
in the database. (These will mostly be like ActiveRecord scopes.)
|
81
|
+
|
82
|
+
~~~ ruby
|
83
|
+
UserRepository = Preserves.repository(model: User) do
|
84
|
+
mapping do
|
85
|
+
map id: 'username' # The database field named 'username' corresponds to the 'id' attribute in the model.
|
86
|
+
map :age, Integer # The 'age' field should be mapped to an Integer in the model.
|
87
|
+
end
|
88
|
+
|
89
|
+
# We'll likely provide `insert`, but this gives an idea of how minimal we'll be to start off.
|
90
|
+
def insert(user)
|
91
|
+
result = query("INSERT INTO 'users' (username, name, age) VALUES ($1, $2, $3)",
|
92
|
+
user.id, user.name, user.age)
|
93
|
+
raise "Could not insert User #{user.id} into database" unless result.size == 1
|
94
|
+
end
|
95
|
+
|
96
|
+
def older_than(age)
|
97
|
+
select("SELECT *, username AS id FROM 'users' WHERE age > $1 ORDER BY $2", age, :name)
|
98
|
+
end
|
99
|
+
|
100
|
+
def with_id(id)
|
101
|
+
select("SELECT *, username AS id FROM 'users' WHERE username = $1", id)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
~~~
|
105
|
+
|
106
|
+
Now we can create model objects and use the repository to save them to and
|
107
|
+
retrieve them from the database:
|
108
|
+
|
109
|
+
~~~ ruby
|
110
|
+
craig = User.new("booch", "Craig", 42)
|
111
|
+
UserRepository.insert(craig)
|
112
|
+
users_over_40 = UserRepository.older_than(40) # Returns an Enumerable set of User objects.
|
113
|
+
beth = UserRepository.with_id("beth").one # Returns a single User object or nil.
|
114
|
+
~~~
|
115
|
+
|
116
|
+
|
117
|
+
API Summary
|
118
|
+
-----------
|
119
|
+
|
120
|
+
NOTE: This project is in very early exploratory stages. The API **will** change.
|
121
|
+
|
122
|
+
|
123
|
+
### Repository ###
|
124
|
+
|
125
|
+
Most of the API you'll use will be in the your repository object.
|
126
|
+
The mixin provides the following methods:
|
127
|
+
|
128
|
+
~~~ ruby
|
129
|
+
fetch(id) # Fetch a single domain model object by its primary key.
|
130
|
+
[id] # Fetch a single domain model object by its primary key.
|
131
|
+
query(sql_string) # Runs SQL and returns a Preserves::SQL::ResultSet.
|
132
|
+
select(sql_string) # Runs SQL and returns a Preserves::Selection.
|
133
|
+
select(sql_string, param1, param2) # Include bind params for the SQL query.
|
134
|
+
select(sql_string, association_name: sql_result) # Include associations.
|
135
|
+
~~~
|
136
|
+
|
137
|
+
|
138
|
+
### Preserves::SQL::ResultSet ###
|
139
|
+
|
140
|
+
~~~ ruby
|
141
|
+
result.size # Number of rows that were affected by the SQL query.
|
142
|
+
~~~
|
143
|
+
|
144
|
+
|
145
|
+
### Preserves::Selection ###
|
146
|
+
|
147
|
+
A Selection is an Enumerable, representing the results of a SELECT query,
|
148
|
+
mapped to domain model objects.
|
149
|
+
Most of your interactions with Selections will be through the Enumerable interface.
|
150
|
+
|
151
|
+
~~~ ruby
|
152
|
+
selection.each # Iterates through the resulting domain objects.
|
153
|
+
selection.first # Returns the first result. Returns nil if there are no results.
|
154
|
+
selection.first! # Returns the first result. Raises an exception if there are no results.
|
155
|
+
selection.last # Returns the last result. Returns nil if there are no results.
|
156
|
+
selection.last! # Returns the last result. Raises an exception if there are no results.
|
157
|
+
selection.only # Returns the only result. Returns nil if there are no results. Raises an exception if there's more than 1 result. (Aliased as `one`.)
|
158
|
+
selection.only! # Returns the only result. Raises an exception if there's not exactly 1 result. (Aliased as `one!`.)
|
159
|
+
~~~
|
160
|
+
|
161
|
+
|
162
|
+
Contributing
|
163
|
+
------------
|
164
|
+
|
165
|
+
1. Fork the [project repo].
|
166
|
+
2. Create your feature branch (`git checkout -b my-new-feature`).
|
167
|
+
3. Make sure tests pass (`rspec` or `rake spec`).
|
168
|
+
4. Commit your changes (`git commit -am 'Add some feature'`).
|
169
|
+
5. Push to the branch (`git push origin my-new-feature`).
|
170
|
+
6. Create a new [pull request].
|
171
|
+
|
172
|
+
|
173
|
+
[Struct]: http://ruby-doc.org/core-2.2.0/Struct.html
|
174
|
+
[OpenStruct]: http://ruby-doc.org/stdlib-2.2.0/libdoc/ostruct/rdoc/OpenStruct.html
|
175
|
+
[Virtus]: https://github.com/solnic/virtus#readme
|
176
|
+
|
177
|
+
[project repo]: https://github.com/boochtek/ruby_preserves/fork
|
178
|
+
[pull request]: https://github.com/boochtek/ruby_preserves/pulls
|
data/Rakefile
ADDED
data/SETUP.sh
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
case "$(uname)" in
|
4
|
+
Darwin)
|
5
|
+
brew install postgresql
|
6
|
+
ln -sfv /usr/local/opt/postgresql/*.plist ~/Library/LaunchAgents
|
7
|
+
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.postgresql.plist
|
8
|
+
bundle --path vendor/bundle
|
9
|
+
createdb 'preserves_test'
|
10
|
+
;;
|
11
|
+
*)
|
12
|
+
echo "Don't know how to install on this system. Please submit a pull request."
|
13
|
+
exit
|
14
|
+
;;
|
15
|
+
esac
|
data/TODO.md
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
TODO
|
2
|
+
====
|
3
|
+
|
4
|
+
|
5
|
+
Presentation
|
6
|
+
------------
|
7
|
+
|
8
|
+
* Create the short URL. (NO UNDERSCORES on TinyURL)
|
9
|
+
* http://craigbuchek.com/ruby-preserves-rubyconf
|
10
|
+
* http://tinyurl.com/ruby-preserves-rubyconf
|
11
|
+
* https://rawgit.com/booch/presentations/Ruby_Preserves-RubyConf-2015-11-15/Ruby_Preserves/slides.html
|
12
|
+
|
13
|
+
|
14
|
+
ASAP
|
15
|
+
----
|
16
|
+
|
17
|
+
* README: Document has_many and belongs_to.
|
18
|
+
* Advise to avoid belongs_to mappings, if possible.
|
19
|
+
* Especially don't want circular dependencies.
|
20
|
+
* README: Show an example of pagination.
|
21
|
+
* README: Show how to use a different repository for tests, if necessary.
|
22
|
+
* Since we require SQL, we can't really do in-memory.
|
23
|
+
* So the repository would not be a Preserves repository.
|
24
|
+
* Saving.
|
25
|
+
* Not sure if we should just have subclasses use query().
|
26
|
+
* I'm starting to lean that way.
|
27
|
+
* We probably have all the info we need to build the INSERT programmatically.
|
28
|
+
* But that would violate our "just use SQL everywhere" mantra.
|
29
|
+
* insert / update / save / delete
|
30
|
+
* Preserves.repository() should return a module to mix in, and not take a block.
|
31
|
+
* And should not be singletons.
|
32
|
+
* Might use method_missing on class to allow usage as if it's a singleton.
|
33
|
+
* Use the Module Factory pattern.
|
34
|
+
|
35
|
+
|
36
|
+
Soonish
|
37
|
+
-------
|
38
|
+
|
39
|
+
* Convenience methods.
|
40
|
+
* create_table
|
41
|
+
* scope
|
42
|
+
* More coercions.
|
43
|
+
* Boolean
|
44
|
+
* Date
|
45
|
+
* Rename to serialize/deserialize.
|
46
|
+
* Move serializers to their own class(es).
|
47
|
+
* Or should we use solnic/coercible gem?
|
48
|
+
* Allow a way to specify more type mappings/serializers.
|
49
|
+
* Registration?
|
50
|
+
* Get default mappings from DB schema.
|
51
|
+
* INTEGER
|
52
|
+
* DATE
|
53
|
+
* TIME
|
54
|
+
* Prepared statements.
|
55
|
+
* Can we just prepare every SQL query we run?
|
56
|
+
* Have a cache mapping the SQL query string to the prepared statement.
|
57
|
+
* Would obviously want to make this a LRU cache eventually.
|
58
|
+
* Ensure we can use PostgreSQL arrays.
|
59
|
+
* Be consistent between strings and symbols.
|
60
|
+
* Can we initialize the domain model objects, instead of using setters?
|
61
|
+
* Would initialize with a Hash of attributes and values.
|
62
|
+
* Might allow both variants, to work with different kinds of classes.
|
63
|
+
* Would need a way to know if the model class supports the initializer we'd be using.
|
64
|
+
* Allow strings in place of class names for specifying repositories.
|
65
|
+
* Because we'll have circular references for belongs_to/has_many pairs.
|
66
|
+
* Or should we not allow that, because it's bad for OOP to have circular dependencies?
|
67
|
+
* Better exceptions -- add some Exception classes.
|
68
|
+
* Have Selection class lazily do mapping, instead of eagerly in the repository?
|
69
|
+
* Unit tests.
|
70
|
+
* We currently only have integration/acceptance tests.
|
71
|
+
* Pluralize.
|
72
|
+
* Will have to move it to its own file and make it public.
|
73
|
+
* Add has_many :through relations.
|
74
|
+
* Might already work with the existing code, and just need testing.
|
75
|
+
* Allow, but don't require, join table to have an associated Repository object.
|
76
|
+
* Use ActiveRecord syntax, but store them separately in Mapper.
|
77
|
+
* Should we catch exceptions from the DB?
|
78
|
+
* Should we reraise them with our own exception class?
|
79
|
+
* Should we swallow them?
|
80
|
+
* Cleanup.
|
81
|
+
* Clean up Mapper a bit more.
|
82
|
+
* Setting up the DB in spec_helper is terrible.
|
83
|
+
* At least move it to a separate file.
|
84
|
+
* Better documentation.
|
85
|
+
* README isn't great at explaining how to use it.
|
86
|
+
* Should make recommendations on how to use this.
|
87
|
+
* In Rails, recommend putting repositories in app/repositories/.
|
88
|
+
* Add that to LOAD_PATH, but don't auto-load.
|
89
|
+
* Repository file should require domain model file, but never vice-versa.
|
90
|
+
* Recommend they consider using 'Users' instead of 'UserRepository'.
|
91
|
+
* Handle Virtus models.
|
92
|
+
* Get list of default mappings from model attributes list.
|
93
|
+
* New up the object with all attributes, instead of setting them individually.
|
94
|
+
* Will probably make this a separate gem.
|
95
|
+
* Can layer on top, or inject extra strategies for object creation and default mappings.
|
96
|
+
* Identity map.
|
97
|
+
* For cases where we're creating a bunch of objects, but some already exist.
|
98
|
+
* Allow a way to specify that a model is a value type (which doesn't have an identity).
|
99
|
+
* Does it make sense to have these in the database?
|
100
|
+
* Test for mapping both type and name.
|
101
|
+
* Probably already works.
|
102
|
+
* Fit into ActiveModel.
|
103
|
+
* Would require picking one base class for models.
|
104
|
+
* Would lose the ability to use POROs (at least when using ActiveModel).
|
105
|
+
* Would require mutual dependencies.
|
106
|
+
* The model will have to call to the repo to persist itself.
|
107
|
+
* The repo will need to know about the model.
|
108
|
+
* Maybe there's a way to actually break this, since our mapping doesn't need it immediately.
|
109
|
+
* Would require also including a validation layer (I think).
|
110
|
+
* Connection pooling.
|
111
|
+
* Is there a way we could do dirty tracking/updating?
|
112
|
+
* This would require some help from the model.
|
113
|
+
* So we'd probably make it optional, depending on whether the model supports it.
|
114
|
+
* Is there a way we could do optimistic locking?
|
115
|
+
* Is there a way we could do lazy loading of associations?
|
116
|
+
* Transactions / Unit of Work.
|
117
|
+
* Composite keys.
|
118
|
+
* Use Mutant for testing.
|
119
|
+
* Use a CI service.
|
120
|
+
* Use Ruby 2.1 keyword arguments.
|
121
|
+
* Use cursors.
|
122
|
+
* Performance testing.
|
data/lib/preserves.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require "preserves/version"
|
2
|
+
require "preserves/repository"
|
3
|
+
require "preserves/sql"
|
4
|
+
|
5
|
+
|
6
|
+
module Preserves
|
7
|
+
def self.repository(options={}, &block)
|
8
|
+
repository = Repository.new(options)
|
9
|
+
repository.instance_eval(&block)
|
10
|
+
repository
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.data_store=(connection)
|
14
|
+
@data_store = connection
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.data_store
|
18
|
+
@data_store or raise "You must define a default data store"
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.PostgreSQL(db_name)
|
22
|
+
SQL.connection(dbname: db_name)
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# Terminology note: A field is associated with a single row/record. A column pertains to all rows/records.
|
2
|
+
|
3
|
+
require "preserves/mapping"
|
4
|
+
require "preserves/mapper/has_many"
|
5
|
+
require "preserves/mapper/belongs_to"
|
6
|
+
|
7
|
+
|
8
|
+
module Preserves
|
9
|
+
class Mapper
|
10
|
+
|
11
|
+
attr_accessor :mapping
|
12
|
+
|
13
|
+
def initialize(mapping)
|
14
|
+
self.mapping = mapping
|
15
|
+
end
|
16
|
+
|
17
|
+
def map(result, relations={})
|
18
|
+
result.map do |record|
|
19
|
+
map_one(record, relations)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def map_one(record, relations={})
|
24
|
+
mapping.model_class.new.tap do |object|
|
25
|
+
map_attributes(object, record)
|
26
|
+
map_relations(object, record, relations)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def primary_key_attribute
|
33
|
+
column_name_to_attribute_name(mapping.primary_key)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def map_attributes(object, record)
|
39
|
+
record.each_pair do |column_name, field_value|
|
40
|
+
attribute_name = column_name_to_attribute_name(column_name)
|
41
|
+
if object.respond_to?("#{attribute_name}=")
|
42
|
+
object.send("#{attribute_name}=", field_value_to_attribute_value(attribute_name, field_value))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def column_name_to_attribute_name(column_name)
|
48
|
+
mapping.name_mappings.fetch(column_name) { column_name }
|
49
|
+
end
|
50
|
+
|
51
|
+
def field_value_to_attribute_value(attribute_name, field_value)
|
52
|
+
attribute_type = attribute_name_to_attribute_type(attribute_name)
|
53
|
+
coerce(field_value, to: attribute_type)
|
54
|
+
end
|
55
|
+
|
56
|
+
def attribute_value_to_field_value(attribute_name, attribute_value)
|
57
|
+
attribute_type = attribute_name_to_attribute_type(attribute_name)
|
58
|
+
uncoerce(attribute_value, to: attribute_type)
|
59
|
+
end
|
60
|
+
|
61
|
+
def attribute_name_to_attribute_type(attribute_name)
|
62
|
+
mapping.type_mappings.fetch(attribute_name.to_sym) { String }
|
63
|
+
end
|
64
|
+
|
65
|
+
def map_relations(object, record, relations)
|
66
|
+
has_many_relations = relations.select{ |k, _v| mapping.has_many_mappings.keys.include?(k) }
|
67
|
+
map_has_many_relations(object, record, has_many_relations)
|
68
|
+
belongs_to_relations = relations.select{ |k, _v| mapping.belongs_to_mappings.keys.include?(k) }
|
69
|
+
map_belongs_to_relations(object, record, belongs_to_relations)
|
70
|
+
# TODO: Raise an exception if any of the relations weren't found in any of the relation mappings.
|
71
|
+
end
|
72
|
+
|
73
|
+
def map_has_many_relations(object, record, relations)
|
74
|
+
# TODO: Ensure that there's a setter for every relation_name before we iterate through the relations.
|
75
|
+
relations.each do |relation_name, relation_result_set|
|
76
|
+
Mapper::HasMany.new(object, record, relation_name, relation_result_set, mapping).map!
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def map_belongs_to_relations(object, record, relations)
|
81
|
+
# TODO: Ensure that there's a setter for every relation_name before we iterate through the relations.
|
82
|
+
relations.each do |relation_name, relation_result_set|
|
83
|
+
Mapper::BelongsTo.new(object, record, relation_name, relation_result_set, mapping).map!
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def coerce(field_value, options={})
|
88
|
+
return nil if field_value.nil?
|
89
|
+
|
90
|
+
if options[:to] == Integer
|
91
|
+
Integer(field_value)
|
92
|
+
else
|
93
|
+
field_value
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def uncoerce(attribute_value, options={})
|
98
|
+
return "NULL" if attribute_value.nil?
|
99
|
+
|
100
|
+
if options[:to] == String
|
101
|
+
"'#{attribute_value}'"
|
102
|
+
else
|
103
|
+
attribute_value.to_s
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|