sbf-dm-core 1.3.0.beta
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 +7 -0
- data/.autotest +29 -0
- data/.document +5 -0
- data/.gitignore +44 -0
- data/.rspec +1 -0
- data/.rubocop.yml +468 -0
- data/.travis.yml +57 -0
- data/.yardopts +1 -0
- data/Gemfile +70 -0
- data/LICENSE +20 -0
- data/README.md +269 -0
- data/Rakefile +4 -0
- data/dm-core.gemspec +21 -0
- data/lib/dm-core/adapters/abstract_adapter.rb +233 -0
- data/lib/dm-core/adapters/in_memory_adapter.rb +110 -0
- data/lib/dm-core/adapters.rb +249 -0
- data/lib/dm-core/associations/many_to_many.rb +477 -0
- data/lib/dm-core/associations/many_to_one.rb +282 -0
- data/lib/dm-core/associations/one_to_many.rb +332 -0
- data/lib/dm-core/associations/one_to_one.rb +84 -0
- data/lib/dm-core/associations/relationship.rb +650 -0
- data/lib/dm-core/backwards.rb +11 -0
- data/lib/dm-core/collection.rb +1486 -0
- data/lib/dm-core/core_ext/kernel.rb +21 -0
- data/lib/dm-core/core_ext/pathname.rb +4 -0
- data/lib/dm-core/core_ext/symbol.rb +10 -0
- data/lib/dm-core/identity_map.rb +6 -0
- data/lib/dm-core/model/hook.rb +99 -0
- data/lib/dm-core/model/is.rb +30 -0
- data/lib/dm-core/model/property.rb +244 -0
- data/lib/dm-core/model/relationship.rb +366 -0
- data/lib/dm-core/model/scope.rb +87 -0
- data/lib/dm-core/model.rb +876 -0
- data/lib/dm-core/property/binary.rb +19 -0
- data/lib/dm-core/property/boolean.rb +35 -0
- data/lib/dm-core/property/class.rb +23 -0
- data/lib/dm-core/property/date.rb +45 -0
- data/lib/dm-core/property/date_time.rb +44 -0
- data/lib/dm-core/property/decimal.rb +47 -0
- data/lib/dm-core/property/discriminator.rb +40 -0
- data/lib/dm-core/property/float.rb +27 -0
- data/lib/dm-core/property/integer.rb +32 -0
- data/lib/dm-core/property/invalid_value_error.rb +17 -0
- data/lib/dm-core/property/lookup.rb +26 -0
- data/lib/dm-core/property/numeric.rb +35 -0
- data/lib/dm-core/property/object.rb +33 -0
- data/lib/dm-core/property/serial.rb +13 -0
- data/lib/dm-core/property/string.rb +47 -0
- data/lib/dm-core/property/text.rb +12 -0
- data/lib/dm-core/property/time.rb +46 -0
- data/lib/dm-core/property/typecast/numeric.rb +32 -0
- data/lib/dm-core/property/typecast/time.rb +33 -0
- data/lib/dm-core/property.rb +856 -0
- data/lib/dm-core/property_set.rb +177 -0
- data/lib/dm-core/query/conditions/comparison.rb +886 -0
- data/lib/dm-core/query/conditions/operation.rb +710 -0
- data/lib/dm-core/query/direction.rb +33 -0
- data/lib/dm-core/query/operator.rb +34 -0
- data/lib/dm-core/query/path.rb +113 -0
- data/lib/dm-core/query/sort.rb +38 -0
- data/lib/dm-core/query.rb +1352 -0
- data/lib/dm-core/relationship_set.rb +69 -0
- data/lib/dm-core/repository.rb +226 -0
- data/lib/dm-core/resource/persistence_state/clean.rb +36 -0
- data/lib/dm-core/resource/persistence_state/deleted.rb +26 -0
- data/lib/dm-core/resource/persistence_state/dirty.rb +91 -0
- data/lib/dm-core/resource/persistence_state/immutable.rb +32 -0
- data/lib/dm-core/resource/persistence_state/persisted.rb +25 -0
- data/lib/dm-core/resource/persistence_state/transient.rb +87 -0
- data/lib/dm-core/resource/persistence_state.rb +70 -0
- data/lib/dm-core/resource.rb +1220 -0
- data/lib/dm-core/spec/lib/adapter_helpers.rb +63 -0
- data/lib/dm-core/spec/lib/collection_helpers.rb +21 -0
- data/lib/dm-core/spec/lib/counter_adapter.rb +38 -0
- data/lib/dm-core/spec/lib/pending_helpers.rb +50 -0
- data/lib/dm-core/spec/lib/spec_helper.rb +74 -0
- data/lib/dm-core/spec/setup.rb +164 -0
- data/lib/dm-core/spec/shared/adapter_spec.rb +366 -0
- data/lib/dm-core/spec/shared/public/property_spec.rb +229 -0
- data/lib/dm-core/spec/shared/resource_spec.rb +1221 -0
- data/lib/dm-core/spec/shared/sel_spec.rb +111 -0
- data/lib/dm-core/spec/shared/semipublic/property_spec.rb +184 -0
- data/lib/dm-core/spec/shared/semipublic/query/conditions/abstract_comparison_spec.rb +261 -0
- data/lib/dm-core/support/assertions.rb +8 -0
- data/lib/dm-core/support/chainable.rb +18 -0
- data/lib/dm-core/support/deprecate.rb +12 -0
- data/lib/dm-core/support/descendant_set.rb +89 -0
- data/lib/dm-core/support/equalizer.rb +48 -0
- data/lib/dm-core/support/ext/array.rb +22 -0
- data/lib/dm-core/support/ext/blank.rb +25 -0
- data/lib/dm-core/support/ext/hash.rb +67 -0
- data/lib/dm-core/support/ext/module.rb +47 -0
- data/lib/dm-core/support/ext/object.rb +57 -0
- data/lib/dm-core/support/ext/string.rb +24 -0
- data/lib/dm-core/support/ext/try_dup.rb +12 -0
- data/lib/dm-core/support/hook.rb +388 -0
- data/lib/dm-core/support/inflections.rb +60 -0
- data/lib/dm-core/support/inflector/inflections.rb +211 -0
- data/lib/dm-core/support/inflector/methods.rb +151 -0
- data/lib/dm-core/support/lazy_array.rb +451 -0
- data/lib/dm-core/support/local_object_space.rb +13 -0
- data/lib/dm-core/support/logger.rb +201 -0
- data/lib/dm-core/support/mash.rb +176 -0
- data/lib/dm-core/support/naming_conventions.rb +109 -0
- data/lib/dm-core/support/ordered_set.rb +381 -0
- data/lib/dm-core/support/subject.rb +33 -0
- data/lib/dm-core/support/subject_set.rb +251 -0
- data/lib/dm-core/version.rb +3 -0
- data/lib/dm-core.rb +274 -0
- data/script/performance.rb +275 -0
- data/script/profile.rb +218 -0
- data/spec/lib/rspec_immediate_feedback_formatter.rb +54 -0
- data/spec/public/associations/many_to_many/read_multiple_join_spec.rb +69 -0
- data/spec/public/associations/many_to_many_spec.rb +197 -0
- data/spec/public/associations/many_to_one_spec.rb +83 -0
- data/spec/public/associations/many_to_one_with_boolean_cpk_spec.rb +40 -0
- data/spec/public/associations/many_to_one_with_custom_fk_spec.rb +49 -0
- data/spec/public/associations/one_to_many_spec.rb +81 -0
- data/spec/public/associations/one_to_one_spec.rb +176 -0
- data/spec/public/associations/one_to_one_with_boolean_cpk_spec.rb +46 -0
- data/spec/public/collection_spec.rb +69 -0
- data/spec/public/finalize_spec.rb +77 -0
- data/spec/public/model/hook_spec.rb +245 -0
- data/spec/public/model/property_spec.rb +91 -0
- data/spec/public/model/relationship_spec.rb +1040 -0
- data/spec/public/model_spec.rb +456 -0
- data/spec/public/property/binary_spec.rb +43 -0
- data/spec/public/property/boolean_spec.rb +21 -0
- data/spec/public/property/class_spec.rb +27 -0
- data/spec/public/property/date_spec.rb +21 -0
- data/spec/public/property/date_time_spec.rb +21 -0
- data/spec/public/property/decimal_spec.rb +23 -0
- data/spec/public/property/discriminator_spec.rb +134 -0
- data/spec/public/property/float_spec.rb +22 -0
- data/spec/public/property/integer_spec.rb +22 -0
- data/spec/public/property/object_spec.rb +117 -0
- data/spec/public/property/serial_spec.rb +22 -0
- data/spec/public/property/string_spec.rb +21 -0
- data/spec/public/property/text_spec.rb +62 -0
- data/spec/public/property/time_spec.rb +21 -0
- data/spec/public/property_spec.rb +333 -0
- data/spec/public/resource/state_spec.rb +72 -0
- data/spec/public/resource_spec.rb +289 -0
- data/spec/public/sel_spec.rb +53 -0
- data/spec/public/setup_spec.rb +145 -0
- data/spec/public/shared/association_collection_shared_spec.rb +309 -0
- data/spec/public/shared/collection_finder_shared_spec.rb +267 -0
- data/spec/public/shared/collection_shared_spec.rb +1637 -0
- data/spec/public/shared/finder_shared_spec.rb +1647 -0
- data/spec/semipublic/adapters/abstract_adapter_spec.rb +30 -0
- data/spec/semipublic/adapters/in_memory_adapter_spec.rb +13 -0
- data/spec/semipublic/associations/many_to_many_spec.rb +94 -0
- data/spec/semipublic/associations/many_to_one_spec.rb +63 -0
- data/spec/semipublic/associations/one_to_many_spec.rb +55 -0
- data/spec/semipublic/associations/one_to_one_spec.rb +53 -0
- data/spec/semipublic/associations/relationship_spec.rb +200 -0
- data/spec/semipublic/associations_spec.rb +177 -0
- data/spec/semipublic/collection_spec.rb +110 -0
- data/spec/semipublic/model_spec.rb +96 -0
- data/spec/semipublic/property/binary_spec.rb +13 -0
- data/spec/semipublic/property/boolean_spec.rb +47 -0
- data/spec/semipublic/property/class_spec.rb +33 -0
- data/spec/semipublic/property/date_spec.rb +43 -0
- data/spec/semipublic/property/date_time_spec.rb +46 -0
- data/spec/semipublic/property/decimal_spec.rb +83 -0
- data/spec/semipublic/property/discriminator_spec.rb +19 -0
- data/spec/semipublic/property/float_spec.rb +82 -0
- data/spec/semipublic/property/integer_spec.rb +82 -0
- data/spec/semipublic/property/lookup_spec.rb +29 -0
- data/spec/semipublic/property/serial_spec.rb +13 -0
- data/spec/semipublic/property/string_spec.rb +13 -0
- data/spec/semipublic/property/text_spec.rb +31 -0
- data/spec/semipublic/property/time_spec.rb +50 -0
- data/spec/semipublic/property_spec.rb +114 -0
- data/spec/semipublic/query/conditions/comparison_spec.rb +1502 -0
- data/spec/semipublic/query/conditions/operation_spec.rb +1296 -0
- data/spec/semipublic/query/path_spec.rb +471 -0
- data/spec/semipublic/query_spec.rb +3665 -0
- data/spec/semipublic/resource/state/clean_spec.rb +89 -0
- data/spec/semipublic/resource/state/deleted_spec.rb +79 -0
- data/spec/semipublic/resource/state/dirty_spec.rb +163 -0
- data/spec/semipublic/resource/state/immutable_spec.rb +107 -0
- data/spec/semipublic/resource/state/transient_spec.rb +163 -0
- data/spec/semipublic/resource/state_spec.rb +230 -0
- data/spec/semipublic/resource_spec.rb +23 -0
- data/spec/semipublic/shared/condition_shared_spec.rb +9 -0
- data/spec/semipublic/shared/resource_shared_spec.rb +198 -0
- data/spec/semipublic/shared/resource_state_shared_spec.rb +91 -0
- data/spec/semipublic/shared/subject_shared_spec.rb +79 -0
- data/spec/spec_helper.rb +34 -0
- data/spec/support/core_ext/hash.rb +10 -0
- data/spec/support/core_ext/inheritable_attributes.rb +46 -0
- data/spec/support/properties/huge_integer.rb +17 -0
- data/spec/unit/array_spec.rb +23 -0
- data/spec/unit/blank_spec.rb +73 -0
- data/spec/unit/data_mapper/ordered_set/append_spec.rb +26 -0
- data/spec/unit/data_mapper/ordered_set/clear_spec.rb +24 -0
- data/spec/unit/data_mapper/ordered_set/delete_spec.rb +28 -0
- data/spec/unit/data_mapper/ordered_set/each_spec.rb +19 -0
- data/spec/unit/data_mapper/ordered_set/empty_spec.rb +20 -0
- data/spec/unit/data_mapper/ordered_set/entries_spec.rb +22 -0
- data/spec/unit/data_mapper/ordered_set/eql_spec.rb +51 -0
- data/spec/unit/data_mapper/ordered_set/equal_value_spec.rb +84 -0
- data/spec/unit/data_mapper/ordered_set/hash_spec.rb +12 -0
- data/spec/unit/data_mapper/ordered_set/include_spec.rb +23 -0
- data/spec/unit/data_mapper/ordered_set/index_spec.rb +28 -0
- data/spec/unit/data_mapper/ordered_set/initialize_spec.rb +32 -0
- data/spec/unit/data_mapper/ordered_set/merge_spec.rb +36 -0
- data/spec/unit/data_mapper/ordered_set/shared/append_spec.rb +24 -0
- data/spec/unit/data_mapper/ordered_set/shared/clear_spec.rb +9 -0
- data/spec/unit/data_mapper/ordered_set/shared/delete_spec.rb +25 -0
- data/spec/unit/data_mapper/ordered_set/shared/each_spec.rb +17 -0
- data/spec/unit/data_mapper/ordered_set/shared/empty_spec.rb +9 -0
- data/spec/unit/data_mapper/ordered_set/shared/entries_spec.rb +9 -0
- data/spec/unit/data_mapper/ordered_set/shared/include_spec.rb +9 -0
- data/spec/unit/data_mapper/ordered_set/shared/index_spec.rb +13 -0
- data/spec/unit/data_mapper/ordered_set/shared/initialize_spec.rb +28 -0
- data/spec/unit/data_mapper/ordered_set/shared/merge_spec.rb +28 -0
- data/spec/unit/data_mapper/ordered_set/shared/size_spec.rb +13 -0
- data/spec/unit/data_mapper/ordered_set/shared/to_ary_spec.rb +11 -0
- data/spec/unit/data_mapper/ordered_set/size_spec.rb +27 -0
- data/spec/unit/data_mapper/ordered_set/to_ary_spec.rb +23 -0
- data/spec/unit/data_mapper/subject_set/append_spec.rb +47 -0
- data/spec/unit/data_mapper/subject_set/clear_spec.rb +34 -0
- data/spec/unit/data_mapper/subject_set/delete_spec.rb +40 -0
- data/spec/unit/data_mapper/subject_set/each_spec.rb +30 -0
- data/spec/unit/data_mapper/subject_set/empty_spec.rb +31 -0
- data/spec/unit/data_mapper/subject_set/entries_spec.rb +31 -0
- data/spec/unit/data_mapper/subject_set/get_spec.rb +34 -0
- data/spec/unit/data_mapper/subject_set/include_spec.rb +32 -0
- data/spec/unit/data_mapper/subject_set/named_spec.rb +33 -0
- data/spec/unit/data_mapper/subject_set/shared/append_spec.rb +18 -0
- data/spec/unit/data_mapper/subject_set/shared/clear_spec.rb +9 -0
- data/spec/unit/data_mapper/subject_set/shared/delete_spec.rb +9 -0
- data/spec/unit/data_mapper/subject_set/shared/each_spec.rb +9 -0
- data/spec/unit/data_mapper/subject_set/shared/empty_spec.rb +9 -0
- data/spec/unit/data_mapper/subject_set/shared/entries_spec.rb +9 -0
- data/spec/unit/data_mapper/subject_set/shared/get_spec.rb +9 -0
- data/spec/unit/data_mapper/subject_set/shared/include_spec.rb +9 -0
- data/spec/unit/data_mapper/subject_set/shared/named_spec.rb +9 -0
- data/spec/unit/data_mapper/subject_set/shared/size_spec.rb +13 -0
- data/spec/unit/data_mapper/subject_set/shared/to_ary_spec.rb +9 -0
- data/spec/unit/data_mapper/subject_set/shared/values_at_spec.rb +44 -0
- data/spec/unit/data_mapper/subject_set/size_spec.rb +42 -0
- data/spec/unit/data_mapper/subject_set/to_ary_spec.rb +34 -0
- data/spec/unit/data_mapper/subject_set/values_at_spec.rb +57 -0
- data/spec/unit/hash_spec.rb +27 -0
- data/spec/unit/hook_spec.rb +1216 -0
- data/spec/unit/inflections_spec.rb +14 -0
- data/spec/unit/lazy_array_spec.rb +1949 -0
- data/spec/unit/mash_spec.rb +289 -0
- data/spec/unit/module_spec.rb +70 -0
- data/spec/unit/object_spec.rb +38 -0
- data/spec/unit/try_dup_spec.rb +46 -0
- data/tasks/ci.rake +1 -0
- data/tasks/spec.rake +18 -0
- data/tasks/yard.rake +9 -0
- data/tasks/yardstick.rake +19 -0
- metadata +323 -0
data/README.md
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
Why DataMapper?
|
|
2
|
+
===============
|
|
3
|
+
|
|
4
|
+
Open Development
|
|
5
|
+
----------------
|
|
6
|
+
|
|
7
|
+
DataMapper sports a very accessible code-base and a welcoming community.
|
|
8
|
+
Outside contributions and feedback are welcome and encouraged, especially
|
|
9
|
+
constructive criticism. Make your voice heard! [Submit an issue](https://github.com/firespring/dm-core/issues),
|
|
10
|
+
speak up on our [mailing-list](http://groups.google.com/group/datamapper/),
|
|
11
|
+
chat with us on [irc](irc://irc.freenode.net/#datamapper), write a spec, get it
|
|
12
|
+
reviewed, ask for commit rights. It's as easy as that to become a contributor.
|
|
13
|
+
|
|
14
|
+
Identity Map
|
|
15
|
+
------------
|
|
16
|
+
|
|
17
|
+
One row in the data-store should equal one object reference. Pretty simple idea.
|
|
18
|
+
Pretty profound impact. If you run the following code in ActiveRecord you'll
|
|
19
|
+
see all `false` results. Do the same in DataMapper and it's
|
|
20
|
+
`true` all the way down.
|
|
21
|
+
|
|
22
|
+
``` ruby
|
|
23
|
+
repository do
|
|
24
|
+
@parent = Tree.first(:name => 'bob')
|
|
25
|
+
|
|
26
|
+
@parent.children.each do |child|
|
|
27
|
+
puts @parent.equal?(child.parent) # => true
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This makes DataMapper faster and allocate less resources to get things done.
|
|
33
|
+
|
|
34
|
+
Dirty Tracking
|
|
35
|
+
--------------
|
|
36
|
+
|
|
37
|
+
When you save a model back to your data-store, DataMapper will only write
|
|
38
|
+
the fields that actually changed. So it plays well with others. You can
|
|
39
|
+
use it in an Integration data-store without worrying that your application will
|
|
40
|
+
be a bad actor causing trouble for all of your other processes.
|
|
41
|
+
|
|
42
|
+
Eager Loading
|
|
43
|
+
-------------
|
|
44
|
+
|
|
45
|
+
Ready for something amazing? The following example executes only two queries
|
|
46
|
+
regardless of how many rows the inner and outer queries return.
|
|
47
|
+
|
|
48
|
+
``` ruby
|
|
49
|
+
repository do
|
|
50
|
+
Zoo.all.each { |zoo| zoo.exhibits.to_a }
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Pretty impressive huh? The idea is that you aren't going to load a set of
|
|
55
|
+
objects and use only an association in just one of them. This should hold up
|
|
56
|
+
pretty well against a 99% rule. When you don't want it to work like this, just
|
|
57
|
+
load the item you want in it's own set. So the DataMapper thinks ahead. We
|
|
58
|
+
like to call it "performant by default". This feature single-handedly wipes
|
|
59
|
+
out the "N+1 Query Problem". No need to specify an `:include` option in
|
|
60
|
+
your finders.
|
|
61
|
+
|
|
62
|
+
Laziness Can Be A Virtue
|
|
63
|
+
------------------------
|
|
64
|
+
|
|
65
|
+
Text fields are expensive in data-stores. They're generally stored in a
|
|
66
|
+
different place than the rest of your data. So instead of a fast sequential
|
|
67
|
+
read from your hard-drive, your data-store server has to hop around all over the
|
|
68
|
+
place to get what it needs. Since ActiveRecord returns everything by default,
|
|
69
|
+
adding a text field to a table slows everything down drastically, across the
|
|
70
|
+
board.
|
|
71
|
+
|
|
72
|
+
Not so with the DataMapper. Text fields are lazily loaded, meaning they
|
|
73
|
+
only load when you need them. If you want more control you can enable or
|
|
74
|
+
disable this feature for any field (not just text-fields) by passing a
|
|
75
|
+
`:lazy` option to your field mapping with a value of `true` or
|
|
76
|
+
`false`.
|
|
77
|
+
|
|
78
|
+
``` ruby
|
|
79
|
+
class Animal
|
|
80
|
+
include DataMapper::Resource
|
|
81
|
+
|
|
82
|
+
property :name, String
|
|
83
|
+
property :description, Text, :lazy => false
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Plus, lazy-loading of Text fields happens automatically and intelligently when
|
|
88
|
+
working with associations. The following only issues 2 queries to load up all
|
|
89
|
+
of the notes fields on each animal:
|
|
90
|
+
|
|
91
|
+
``` ruby
|
|
92
|
+
repository do
|
|
93
|
+
Animal.all.each { |animal| animal.description.to_a }
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Did you notice the `#to_a` call in the above example? That
|
|
98
|
+
was necessary because even DataMapper collections are lazy. If you don't
|
|
99
|
+
iterate over them, or in this case ask them to become Arrays, they won't
|
|
100
|
+
execute until you need them. We needed to call `#to_a` to force
|
|
101
|
+
the lazy load because without it, the above example would have only
|
|
102
|
+
executed one query. This extra bit of laziness can come in very handy,
|
|
103
|
+
for example:
|
|
104
|
+
|
|
105
|
+
``` ruby
|
|
106
|
+
animals = Animal.all
|
|
107
|
+
description = 'foo'
|
|
108
|
+
|
|
109
|
+
animals.each do |animal|
|
|
110
|
+
animal.update(:description => description)
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
In the above example, the Animals won't be retrieved until you actually
|
|
115
|
+
need them. This comes in handy in cases where you initialize the
|
|
116
|
+
collection before you know if you need it, like in a web app controller.
|
|
117
|
+
|
|
118
|
+
Collection Chaining
|
|
119
|
+
-------------------
|
|
120
|
+
|
|
121
|
+
DataMapper's lazy collections are also handy because you can get the
|
|
122
|
+
same effect as named scopes, without any special syntax, eg:
|
|
123
|
+
|
|
124
|
+
``` ruby
|
|
125
|
+
class Animal
|
|
126
|
+
# ... setup ...
|
|
127
|
+
|
|
128
|
+
def self.mammals
|
|
129
|
+
all(:mammal => true)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def self.zoo(zoo)
|
|
133
|
+
all(:zoo => zoo)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
zoo = Zoo.first(:name => 'Greater Vancouver Zoo')
|
|
138
|
+
|
|
139
|
+
Animal.mammals.zoo(zoo).to_a # => executes one query
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
In the above example, we ask the Animal model for all the mammals,
|
|
143
|
+
and then all the animals in a specific zoo, and DataMapper will chain
|
|
144
|
+
the collection queries together and execute a single query to retrieve
|
|
145
|
+
the matching records. There's no special syntax, and no custom DSLs
|
|
146
|
+
to learn, it's just plain ruby all the way down.
|
|
147
|
+
|
|
148
|
+
You can even use this on association collections, eg:
|
|
149
|
+
|
|
150
|
+
``` ruby
|
|
151
|
+
zoo.animals.mammals.to_a # => executes one query
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Custom Properties
|
|
155
|
+
-----------------
|
|
156
|
+
|
|
157
|
+
With DataMapper it is possible to create custom properties for your models.
|
|
158
|
+
Consider this example:
|
|
159
|
+
|
|
160
|
+
``` ruby
|
|
161
|
+
module DataMapper
|
|
162
|
+
class Property
|
|
163
|
+
class Email < String
|
|
164
|
+
required true
|
|
165
|
+
format /^([\w\.%\+\-]+)@([\w\-]+\.)+([\w]{2,})$/i
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
class User
|
|
171
|
+
include DataMapper::Resource
|
|
172
|
+
|
|
173
|
+
property :id, Serial
|
|
174
|
+
property :email, Email
|
|
175
|
+
end
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
This way there won't be a need to repeat same property options every time you
|
|
179
|
+
add an email to a model. In the example above we create an Email property which
|
|
180
|
+
is just a String with additional pre-configured options: `required` and
|
|
181
|
+
`format`. Please note that it is possible to override these options when
|
|
182
|
+
declaring a property, like this:
|
|
183
|
+
|
|
184
|
+
``` ruby
|
|
185
|
+
class Member
|
|
186
|
+
include DataMapper::Resource
|
|
187
|
+
|
|
188
|
+
property :id, Serial
|
|
189
|
+
property :email, Email, :required => false
|
|
190
|
+
end
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Plays Well With Others
|
|
194
|
+
----------------------
|
|
195
|
+
|
|
196
|
+
In ActiveRecord, all your fields are mapped, whether you want them or not.
|
|
197
|
+
This slows things down. In the DataMapper you define your mappings in your
|
|
198
|
+
model. So instead of an _ALTER TABLE ADD field_ in your data-store, you simply
|
|
199
|
+
add a `property :name, String` to your model. DRY. No schema.rb. No
|
|
200
|
+
migration files to conflict or die without reverting changes. Your model
|
|
201
|
+
drives the data-store, not the other way around.
|
|
202
|
+
|
|
203
|
+
Unless of course you want to map to a legacy data-store. Raise your hand if you
|
|
204
|
+
like seeing a method called `col2Name` on your model just because
|
|
205
|
+
that's what it's called in an old data-store you can't afford to change right
|
|
206
|
+
now? In DataMapper you control the mappings:
|
|
207
|
+
|
|
208
|
+
``` ruby
|
|
209
|
+
class Fruit
|
|
210
|
+
include DataMapper::Resource
|
|
211
|
+
|
|
212
|
+
storage_names[:repo] = 'frt'
|
|
213
|
+
|
|
214
|
+
property :name, String, :field => 'col2Name'
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
All Ruby, All The Time
|
|
219
|
+
----------------------
|
|
220
|
+
|
|
221
|
+
It's great that ActiveRecord allows you to write SQL when you need to, but
|
|
222
|
+
should we have to so often?
|
|
223
|
+
|
|
224
|
+
DataMapper supports issuing your own query, but it also provides more helpers
|
|
225
|
+
and a unique hash-based condition syntax to cover more of the use-cases where
|
|
226
|
+
issuing your own SQL would have been the only way to go. For example, any
|
|
227
|
+
finder option that's non-standard is considered a condition. So you can write
|
|
228
|
+
`Zoo.all(:name => 'Dallas')` and DataMapper will look for zoos with the
|
|
229
|
+
name of 'Dallas'.
|
|
230
|
+
|
|
231
|
+
It's just a little thing, but it's so much nicer than writing
|
|
232
|
+
`Zoo.find(:all, :conditions => ['name = ?', 'Dallas'])`. What if you
|
|
233
|
+
need other comparisons though? Try these:
|
|
234
|
+
|
|
235
|
+
``` ruby
|
|
236
|
+
# 'gt' means greater-than. We also do 'lt'.
|
|
237
|
+
Person.all(:age.gt => 30)
|
|
238
|
+
|
|
239
|
+
# 'gte' means greather-than-or-equal-to. We also do 'lte'.
|
|
240
|
+
Person.all(:age.gte => 30)
|
|
241
|
+
|
|
242
|
+
# 'not' allows you to match all people without the name "bob"
|
|
243
|
+
Person.all(:name.not => 'bob')
|
|
244
|
+
|
|
245
|
+
# If the value of a pair is an Array, we do an IN-clause for you.
|
|
246
|
+
Person.all(:name.like => 'S%', :id => [ 1, 2, 3, 4, 5 ])
|
|
247
|
+
|
|
248
|
+
# Does a NOT IN () clause for you.
|
|
249
|
+
Person.all(:name.not => [ 'bob', 'rick', 'steve' ])
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
See? Fewer SQL fragments dirtying your Ruby code. And that's just a few of the
|
|
253
|
+
nice syntax tweaks DataMapper delivers out of the box...
|
|
254
|
+
|
|
255
|
+
Note on Patches/Pull Requests
|
|
256
|
+
-----------------------------
|
|
257
|
+
|
|
258
|
+
* Fork the project.
|
|
259
|
+
* Make your feature addition or bug fix.
|
|
260
|
+
* Add tests for it. This is important so I don't break it in a
|
|
261
|
+
future version unintentionally.
|
|
262
|
+
* Commit, do not mess with rakefile, version, or history.
|
|
263
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
|
264
|
+
* Send me a pull request. Bonus points for topic branches.
|
|
265
|
+
|
|
266
|
+
Copyright
|
|
267
|
+
---------
|
|
268
|
+
|
|
269
|
+
Copyright (c) 2012 Dan Kubb. See LICENSE for details.
|
data/Rakefile
ADDED
data/dm-core.gemspec
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require 'English'
|
|
2
|
+
require File.expand_path('lib/dm-core/version', __dir__)
|
|
3
|
+
|
|
4
|
+
Gem::Specification.new do |gem|
|
|
5
|
+
gem.name = 'sbf-dm-core'
|
|
6
|
+
gem.version = DataMapper::VERSION.dup
|
|
7
|
+
gem.required_ruby_version = '>= 2.7.8'
|
|
8
|
+
gem.authors = ['Dan Kubb']
|
|
9
|
+
gem.email = %w(dan.kubb@gmail.com)
|
|
10
|
+
gem.summary = 'DataMapper core library'
|
|
11
|
+
gem.description = 'DataMapper core library where one row in the data-store should equal one object reference. ' \
|
|
12
|
+
'Pretty simple idea. Pretty profound impact.'
|
|
13
|
+
gem.license = 'Nonstandard'
|
|
14
|
+
gem.homepage = 'https://github.com/firespring/dm-core'
|
|
15
|
+
|
|
16
|
+
gem.require_paths = %w(lib)
|
|
17
|
+
gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
|
18
|
+
gem.extra_rdoc_files = %w(LICENSE README.md)
|
|
19
|
+
|
|
20
|
+
gem.add_runtime_dependency('addressable', '~> 2.3', '>= 2.3.5')
|
|
21
|
+
end
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
module DataMapper
|
|
2
|
+
module Adapters
|
|
3
|
+
# Specific adapters extend this class and implement
|
|
4
|
+
# methods for creating, reading, updating and deleting records.
|
|
5
|
+
#
|
|
6
|
+
# Adapters may only implement method for reading or (less common case)
|
|
7
|
+
# writing. Read only adapter may be useful when one needs to work
|
|
8
|
+
# with legacy data that should not be changed or web services that
|
|
9
|
+
# only provide read access to data (from Wordnet and Medline to
|
|
10
|
+
# Atom and RSS syndication feeds)
|
|
11
|
+
#
|
|
12
|
+
# Note that in case of adapters to relational databases it makes
|
|
13
|
+
# sense to inherit from DataObjectsAdapter class.
|
|
14
|
+
class AbstractAdapter
|
|
15
|
+
include DataMapper::Assertions
|
|
16
|
+
extend Equalizer, DataMapper::Assertions
|
|
17
|
+
|
|
18
|
+
equalize :name, :options, :resource_naming_convention, :field_naming_convention
|
|
19
|
+
|
|
20
|
+
# @api semipublic
|
|
21
|
+
def self.descendants
|
|
22
|
+
@descendants ||= DescendantSet.new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @api private
|
|
26
|
+
def self.inherited(descendant)
|
|
27
|
+
descendants << descendant
|
|
28
|
+
super
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Adapter name
|
|
32
|
+
#
|
|
33
|
+
# @example
|
|
34
|
+
# adapter.name # => :default
|
|
35
|
+
#
|
|
36
|
+
# Note that when you use
|
|
37
|
+
#
|
|
38
|
+
# DataMapper.setup(:default, 'postgres://postgres@127.0.0.1/dm_core_test')
|
|
39
|
+
#
|
|
40
|
+
# the adapter name is currently set to :default
|
|
41
|
+
#
|
|
42
|
+
# @return [Symbol]
|
|
43
|
+
# the adapter name
|
|
44
|
+
#
|
|
45
|
+
# @api semipublic
|
|
46
|
+
attr_reader :name
|
|
47
|
+
|
|
48
|
+
# Options with which adapter was set up
|
|
49
|
+
#
|
|
50
|
+
# @example
|
|
51
|
+
# adapter.options # => { :adapter => 'yaml', :path => '/tmp' }
|
|
52
|
+
#
|
|
53
|
+
# @return [Hash]
|
|
54
|
+
# adapter configuration options
|
|
55
|
+
#
|
|
56
|
+
# @api semipublic
|
|
57
|
+
attr_reader :options
|
|
58
|
+
|
|
59
|
+
# A callable object returning a naming convention for model storage
|
|
60
|
+
#
|
|
61
|
+
# @example
|
|
62
|
+
# adapter.resource_naming_convention # => Proc for model storage name
|
|
63
|
+
#
|
|
64
|
+
# @return [#call]
|
|
65
|
+
# object to return the naming convention for each model
|
|
66
|
+
#
|
|
67
|
+
# @api semipublic
|
|
68
|
+
attr_accessor :resource_naming_convention
|
|
69
|
+
|
|
70
|
+
# A callable object returning a naming convention for property fields
|
|
71
|
+
#
|
|
72
|
+
# @example
|
|
73
|
+
# adapter.field_naming_convention # => Proc for field name
|
|
74
|
+
#
|
|
75
|
+
# @return [#call]
|
|
76
|
+
# object to return the naming convention for each field
|
|
77
|
+
#
|
|
78
|
+
# @api semipublic
|
|
79
|
+
attr_accessor :field_naming_convention
|
|
80
|
+
|
|
81
|
+
# Persists one or many new resources
|
|
82
|
+
#
|
|
83
|
+
# @example
|
|
84
|
+
# adapter.create(collection) # => 1
|
|
85
|
+
#
|
|
86
|
+
# Adapters provide specific implementation of this method
|
|
87
|
+
#
|
|
88
|
+
# @param [Enumerable<Resource>] resources
|
|
89
|
+
# The list of resources (model instances) to create
|
|
90
|
+
#
|
|
91
|
+
# @return [Integer]
|
|
92
|
+
# The number of records that were actually saved into the data-store
|
|
93
|
+
#
|
|
94
|
+
# @api semipublic
|
|
95
|
+
def create(resources)
|
|
96
|
+
raise NotImplementedError, "#{self.class}#create not implemented"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Reads one or many resources from a datastore
|
|
100
|
+
#
|
|
101
|
+
# @example
|
|
102
|
+
# adapter.read(query) # => [ { 'name' => 'Dan Kubb' } ]
|
|
103
|
+
#
|
|
104
|
+
# Adapters provide specific implementation of this method
|
|
105
|
+
#
|
|
106
|
+
# @param [Query] query
|
|
107
|
+
# the query to match resources in the datastore
|
|
108
|
+
#
|
|
109
|
+
# @return [Enumerable<Hash>]
|
|
110
|
+
# an array of hashes to become resources
|
|
111
|
+
#
|
|
112
|
+
# @api semipublic
|
|
113
|
+
def read(query)
|
|
114
|
+
raise NotImplementedError, "#{self.class}#read not implemented"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Updates one or many existing resources
|
|
118
|
+
#
|
|
119
|
+
# @example
|
|
120
|
+
# adapter.update(attributes, collection) # => 1
|
|
121
|
+
#
|
|
122
|
+
# Adapters provide specific implementation of this method
|
|
123
|
+
#
|
|
124
|
+
# @param [Hash(Property => Object)] attributes
|
|
125
|
+
# hash of attribute values to set, keyed by Property
|
|
126
|
+
# @param [Collection] collection
|
|
127
|
+
# collection of records to be updated
|
|
128
|
+
#
|
|
129
|
+
# @return [Integer]
|
|
130
|
+
# the number of records updated
|
|
131
|
+
#
|
|
132
|
+
# @api semipublic
|
|
133
|
+
def update(attributes, collection)
|
|
134
|
+
raise NotImplementedError, "#{self.class}#update not implemented"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Deletes one or many existing resources
|
|
138
|
+
#
|
|
139
|
+
# @example
|
|
140
|
+
# adapter.delete(collection) # => 1
|
|
141
|
+
#
|
|
142
|
+
# Adapters provide specific implementation of this method
|
|
143
|
+
#
|
|
144
|
+
# @param [Collection] collection
|
|
145
|
+
# collection of records to be deleted
|
|
146
|
+
#
|
|
147
|
+
# @return [Integer]
|
|
148
|
+
# the number of records deleted
|
|
149
|
+
#
|
|
150
|
+
# @api semipublic
|
|
151
|
+
def delete(collection)
|
|
152
|
+
raise NotImplementedError, "#{self.class}#delete not implemented"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Create a Query object or subclass.
|
|
156
|
+
#
|
|
157
|
+
# Alter this method if you'd like to return an adapter specific Query subclass.
|
|
158
|
+
#
|
|
159
|
+
# @param [Repository] repository
|
|
160
|
+
# the Repository to retrieve results from
|
|
161
|
+
# @param [Model] model
|
|
162
|
+
# the Model to retrieve results from
|
|
163
|
+
# @param [Hash] options
|
|
164
|
+
# the conditions and scope
|
|
165
|
+
#
|
|
166
|
+
# @return [Query]
|
|
167
|
+
#
|
|
168
|
+
# @api semipublic
|
|
169
|
+
#--
|
|
170
|
+
# TODO: DataObjects::Connection.create_command style magic (Adapter)::Query?
|
|
171
|
+
def new_query(repository, model, options = {})
|
|
172
|
+
Query.new(repository, model, options)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Set the serial value of the Resource
|
|
176
|
+
#
|
|
177
|
+
# @param [Resource] resource
|
|
178
|
+
# the resource to set the serial property in
|
|
179
|
+
# @param [Integer] next_id
|
|
180
|
+
# the identifier to set in the resource
|
|
181
|
+
#
|
|
182
|
+
# @return [undefined]
|
|
183
|
+
#
|
|
184
|
+
# @api semipublic
|
|
185
|
+
protected def initialize_serial(resource, next_id)
|
|
186
|
+
return unless (serial = resource.model.serial(name))
|
|
187
|
+
return unless serial.get!(resource).nil?
|
|
188
|
+
|
|
189
|
+
serial.set!(resource, next_id)
|
|
190
|
+
|
|
191
|
+
# TODO: replace above with this, once
|
|
192
|
+
# specs can handle random, non-sequential ids
|
|
193
|
+
# serial.set!(resource, rand(2**32))
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Translate the attributes into a Hash with the field as the key
|
|
197
|
+
#
|
|
198
|
+
# @example
|
|
199
|
+
# attributes = { User.properties[:name] => 'Dan Kubb' }
|
|
200
|
+
# adapter.attributes_as_fields(attributes) # => { 'name' => 'Dan Kubb' }
|
|
201
|
+
#
|
|
202
|
+
# @param [Hash] attributes
|
|
203
|
+
# the attributes with the Property as the key
|
|
204
|
+
#
|
|
205
|
+
# @return [Hash]
|
|
206
|
+
# the attributes with the Property#field as the key
|
|
207
|
+
#
|
|
208
|
+
# @api semipublic
|
|
209
|
+
protected def attributes_as_fields(attributes)
|
|
210
|
+
attributes.to_h { |property, value| [property.field, property.dump(value)] }
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Initialize an AbstractAdapter instance
|
|
214
|
+
#
|
|
215
|
+
# @param [Symbol] name
|
|
216
|
+
# the adapter repository name
|
|
217
|
+
# @param [Hash] options
|
|
218
|
+
# the adapter configuration options
|
|
219
|
+
#
|
|
220
|
+
# @return [undefined]
|
|
221
|
+
#
|
|
222
|
+
# @api semipublic
|
|
223
|
+
private def initialize(name, options)
|
|
224
|
+
@name = name
|
|
225
|
+
@options = options.dup.freeze
|
|
226
|
+
@resource_naming_convention = NamingConventions::Resource::UnderscoredAndPluralized
|
|
227
|
+
@field_naming_convention = NamingConventions::Field::Underscored
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
const_added(:AbstractAdapter)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
module DataMapper
|
|
2
|
+
module Adapters
|
|
3
|
+
# This is probably the simplest functional adapter possible. It simply
|
|
4
|
+
# stores and queries from a hash containing the model classes as keys,
|
|
5
|
+
# and an array of hashes. It is not persistent whatsoever; when the Ruby
|
|
6
|
+
# process finishes, everything that was stored it lost. However, it doesn't
|
|
7
|
+
# require any other external libraries, such as data_objects, so it is ideal
|
|
8
|
+
# for writing specs against. It also serves as an excellent example for
|
|
9
|
+
# budding adapter developers, so it is critical that it remains well documented
|
|
10
|
+
# and up to date.
|
|
11
|
+
class InMemoryAdapter < AbstractAdapter
|
|
12
|
+
# Used by DataMapper to put records into a data-store: "INSERT" in SQL-speak.
|
|
13
|
+
# It takes an array of the resources (model instances) to be saved. Resources
|
|
14
|
+
# each have a key that can be used to quickly look them up later without
|
|
15
|
+
# searching, if the adapter supports it.
|
|
16
|
+
#
|
|
17
|
+
# @param [Enumerable(Resource)] resources
|
|
18
|
+
# The set of resources (model instances)
|
|
19
|
+
#
|
|
20
|
+
# @api semipublic
|
|
21
|
+
def create(resources)
|
|
22
|
+
records = records_for(resources.first&.model)
|
|
23
|
+
|
|
24
|
+
resources.each do |resource|
|
|
25
|
+
initialize_serial(resource, records.size.succ)
|
|
26
|
+
records << attributes_as_fields(resource.attributes(nil))
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Looks up one record or a collection of records from the data-store:
|
|
31
|
+
# "SELECT" in SQL.
|
|
32
|
+
#
|
|
33
|
+
# @param [Query] query
|
|
34
|
+
# The query to be used to search for the resources
|
|
35
|
+
#
|
|
36
|
+
# @return [Array]
|
|
37
|
+
# An Array of Hashes containing the key-value pairs for
|
|
38
|
+
# each record
|
|
39
|
+
#
|
|
40
|
+
# @api semipublic
|
|
41
|
+
def read(query)
|
|
42
|
+
query.filter_records(records_for(query.model).dup)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Used by DataMapper to update the attributes on existing records in a
|
|
46
|
+
# data-store: "UPDATE" in SQL-speak. It takes a hash of the attributes
|
|
47
|
+
# to update with, as well as a collection object that specifies which resources
|
|
48
|
+
# should be updated.
|
|
49
|
+
#
|
|
50
|
+
# @param [Hash] attributes
|
|
51
|
+
# A set of key-value pairs of the attributes to update the resources with.
|
|
52
|
+
# @param [DataMapper::Collection] collection
|
|
53
|
+
# The collection of resources to update.
|
|
54
|
+
#
|
|
55
|
+
# @api semipublic
|
|
56
|
+
def update(attributes, collection)
|
|
57
|
+
attributes = attributes_as_fields(attributes)
|
|
58
|
+
read(collection.query)&.each { |record| record.update(attributes) }&.size
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Destroys all the records matching the given query. "DELETE" in SQL.
|
|
62
|
+
#
|
|
63
|
+
# @param [DataMapper::Collection] collection
|
|
64
|
+
# The collection of resources to delete.
|
|
65
|
+
#
|
|
66
|
+
# @return [Integer]
|
|
67
|
+
# The number of records that were deleted.
|
|
68
|
+
#
|
|
69
|
+
# @api semipublic
|
|
70
|
+
def delete(collection)
|
|
71
|
+
records = records_for(collection.model)
|
|
72
|
+
records_to_delete = collection.query.filter_records(records.dup)
|
|
73
|
+
records.replace(records - records_to_delete)
|
|
74
|
+
records_to_delete&.size
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# TODO: consider proper automigrate functionality
|
|
78
|
+
def reset
|
|
79
|
+
@records = {}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Make a new instance of the adapter. The @records ivar is the 'data-store'
|
|
83
|
+
# for this adapter. It is not shared amongst multiple incarnations of this
|
|
84
|
+
# adapter, eg DataMapper.setup(:default, :adapter => :in_memory);
|
|
85
|
+
# DataMapper.setup(:alternate, :adapter => :in_memory) do not share the
|
|
86
|
+
# data-store between them.
|
|
87
|
+
#
|
|
88
|
+
# @param [String, Symbol] name
|
|
89
|
+
# The name of the Repository using this adapter.
|
|
90
|
+
# @param [String, Hash] options
|
|
91
|
+
# The connection uri string, or a hash of options to set up
|
|
92
|
+
# the adapter
|
|
93
|
+
#
|
|
94
|
+
# @api semipublic
|
|
95
|
+
private def initialize(name, options = {})
|
|
96
|
+
super
|
|
97
|
+
@records = {}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# All the records we're storing. This method will look them up by model name
|
|
101
|
+
#
|
|
102
|
+
# @api private
|
|
103
|
+
private def records_for(model)
|
|
104
|
+
@records[model.storage_name(name)] ||= []
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
const_added(:InMemoryAdapter)
|
|
109
|
+
end
|
|
110
|
+
end
|