chimera 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/History.txt +4 -0
  2. data/Manifest.txt +57 -0
  3. data/PostInstall.txt +7 -0
  4. data/README.rdoc +114 -0
  5. data/Rakefile +30 -0
  6. data/doc/NOTES +11 -0
  7. data/doc/examples/config.yml +16 -0
  8. data/doc/redis6379.conf +132 -0
  9. data/lib/chimera.rb +33 -0
  10. data/lib/chimera/associations.rb +146 -0
  11. data/lib/chimera/attributes.rb +52 -0
  12. data/lib/chimera/base.rb +95 -0
  13. data/lib/chimera/config.rb +9 -0
  14. data/lib/chimera/error.rb +12 -0
  15. data/lib/chimera/finders.rb +49 -0
  16. data/lib/chimera/geo_indexes.rb +76 -0
  17. data/lib/chimera/indexes.rb +177 -0
  18. data/lib/chimera/persistence.rb +70 -0
  19. data/lib/chimera/redis_objects.rb +345 -0
  20. data/lib/redis.rb +373 -0
  21. data/lib/redis/counter.rb +94 -0
  22. data/lib/redis/dist_redis.rb +149 -0
  23. data/lib/redis/hash_ring.rb +135 -0
  24. data/lib/redis/helpers/core_commands.rb +46 -0
  25. data/lib/redis/helpers/serialize.rb +25 -0
  26. data/lib/redis/list.rb +122 -0
  27. data/lib/redis/lock.rb +83 -0
  28. data/lib/redis/objects.rb +100 -0
  29. data/lib/redis/objects/counters.rb +132 -0
  30. data/lib/redis/objects/lists.rb +45 -0
  31. data/lib/redis/objects/locks.rb +71 -0
  32. data/lib/redis/objects/sets.rb +46 -0
  33. data/lib/redis/objects/values.rb +56 -0
  34. data/lib/redis/pipeline.rb +21 -0
  35. data/lib/redis/set.rb +156 -0
  36. data/lib/redis/value.rb +35 -0
  37. data/lib/riak_raw.rb +100 -0
  38. data/lib/typhoeus.rb +55 -0
  39. data/lib/typhoeus/.gitignore +1 -0
  40. data/lib/typhoeus/easy.rb +253 -0
  41. data/lib/typhoeus/filter.rb +28 -0
  42. data/lib/typhoeus/hydra.rb +210 -0
  43. data/lib/typhoeus/multi.rb +34 -0
  44. data/lib/typhoeus/remote.rb +306 -0
  45. data/lib/typhoeus/remote_method.rb +108 -0
  46. data/lib/typhoeus/remote_proxy_object.rb +48 -0
  47. data/lib/typhoeus/request.rb +124 -0
  48. data/lib/typhoeus/response.rb +39 -0
  49. data/lib/typhoeus/service.rb +20 -0
  50. data/script/console +10 -0
  51. data/script/destroy +14 -0
  52. data/script/generate +14 -0
  53. data/test/models.rb +49 -0
  54. data/test/test_chimera.rb +238 -0
  55. data/test/test_helper.rb +7 -0
  56. metadata +243 -0
@@ -0,0 +1,4 @@
1
+ === 0.0.1 2010-03-01
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
@@ -0,0 +1,57 @@
1
+ doc/examples/config.yml
2
+ doc/NOTES
3
+ doc/redis6379.conf
4
+ History.txt
5
+ lib/chimera/associations.rb
6
+ lib/chimera/attributes.rb
7
+ lib/chimera/base.rb
8
+ lib/chimera/config.rb
9
+ lib/chimera/error.rb
10
+ lib/chimera/finders.rb
11
+ lib/chimera/geo_indexes.rb
12
+ lib/chimera/indexes.rb
13
+ lib/chimera/persistence.rb
14
+ lib/chimera/redis_objects.rb
15
+ lib/chimera.rb
16
+ lib/redis/counter.rb
17
+ lib/redis/dist_redis.rb
18
+ lib/redis/hash_ring.rb
19
+ lib/redis/helpers
20
+ lib/redis/helpers/core_commands.rb
21
+ lib/redis/helpers/serialize.rb
22
+ lib/redis/list.rb
23
+ lib/redis/lock.rb
24
+ lib/redis/objects
25
+ lib/redis/objects/counters.rb
26
+ lib/redis/objects/lists.rb
27
+ lib/redis/objects/locks.rb
28
+ lib/redis/objects/sets.rb
29
+ lib/redis/objects/values.rb
30
+ lib/redis/objects.rb
31
+ lib/redis/pipeline.rb
32
+ lib/redis/set.rb
33
+ lib/redis/value.rb
34
+ lib/redis.rb
35
+ lib/riak_raw.rb
36
+ lib/typhoeus/.gitignore
37
+ lib/typhoeus/easy.rb
38
+ lib/typhoeus/filter.rb
39
+ lib/typhoeus/hydra.rb
40
+ lib/typhoeus/multi.rb
41
+ lib/typhoeus/remote.rb
42
+ lib/typhoeus/remote_method.rb
43
+ lib/typhoeus/remote_proxy_object.rb
44
+ lib/typhoeus/request.rb
45
+ lib/typhoeus/response.rb
46
+ lib/typhoeus/service.rb
47
+ lib/typhoeus.rb
48
+ Manifest.txt
49
+ PostInstall.txt
50
+ Rakefile
51
+ README.rdoc
52
+ script/console
53
+ script/destroy
54
+ script/generate
55
+ test/models.rb
56
+ test/test_chimera.rb
57
+ test/test_helper.rb
@@ -0,0 +1,7 @@
1
+
2
+ For more information on chimera, see http://chimera.rubyforge.org
3
+
4
+ NOTE: Change this information in PostInstall.txt
5
+ You can also delete it if you don't want it.
6
+
7
+
@@ -0,0 +1,114 @@
1
+ = chimera
2
+
3
+ * http://github.com/#{github_username}/#{project_name}
4
+
5
+ == DESCRIPTION:
6
+
7
+ Chimera is an object mapper for Riak and Redis. The idea is to mix the advantages of Riak
8
+ (scalability, massive data storage) with Redis (atomicity, performance, data structures).
9
+ You should store the bulk of your data in Riak, and then use Redis data structures where
10
+ appropriate (for example, a counter or set of keys).
11
+
12
+ Internally, Chimera uses Redis for any indexes you define as well as some default indexes
13
+ that are automatically created. There's no built in sharding for Redis, but since it's
14
+ only being used for key storage and basic data elements you should be able to go a long
15
+ way with one Redis server (especially if you use the new Redis VM).
16
+
17
+ == FEATURES:
18
+
19
+ * Uses Riak (http://riak.basho.com/) for your primary storage.
20
+ * Uses Redis for indexes and also allows you to define Redis objects on a model.
21
+ * Supports unique and non-unique indexes, as well as basic search indexes (words are stemmed,
22
+ uses set intersection so you can get an AND/OR search).
23
+ * Supports a geospatial index type for simple storage and lookup of coordinates.
24
+
25
+ == ISSUES/NOTES:
26
+
27
+ * Experimental. Not yet tested in production environment, use at your own risk.
28
+ * Test coverage needs to be improved.
29
+ * Documentation is lacking. At the moment reading the tests and sample test/models.rb
30
+ file are your best bet.
31
+
32
+ == SYNOPSIS:
33
+
34
+ require "rubygems"
35
+ gem "chimera", "0.0.1"
36
+ require "chimera"
37
+
38
+ Chimera.config_path = "path/to/your/config.yml"
39
+
40
+ class Car < Chimera::Base
41
+ attribute :vin
42
+ attribute :make
43
+ attribute :model
44
+ attribute :year
45
+ attribute :description
46
+
47
+ index :year, :type => :find
48
+ index :description, :type => :search
49
+ index :vin, :type => :unique
50
+
51
+ validates_presence_of :vin, :make, :model, :year
52
+ end
53
+
54
+ c = Car.new
55
+ c.id = Car.new_uuid
56
+ c.vin = 12345
57
+ c.make = "Pagani"
58
+ c.model = "Zonda Cinque Roadster"
59
+ c.year = 2010
60
+ c.description = "The Roadster will have specs as the Cinque Coupé, and will likely maintain the same rigidity or more, as it was for the Roadster F and F Coupé."
61
+ c.save
62
+
63
+ Car.find_with_index(:year, 2010)
64
+ => [c]
65
+
66
+ Car.find_with_index(:description, "rigid boat", :type => :union)
67
+ => [c]
68
+
69
+ Car.find_with_index(:description, "rigid boat", :type => :intersect)
70
+ => []
71
+
72
+ * See tests for more usage examples
73
+
74
+ == REQUIREMENTS:
75
+
76
+ * Riak Server
77
+ * Redis Server
78
+
79
+ * ActiveSupport+ActiveModel 3.0.0.beta
80
+
81
+ * uuidtools 2.1.1
82
+ * brianmario-yajl-ruby 0.6.3
83
+ * fast-stemmer 1.0.0
84
+
85
+ == INSTALL:
86
+
87
+ $ gem install tzinfo builder memcache-client rack rack-test rack-mount erubis mail text-format thor bundler i18n
88
+ $ gem install rails --pre
89
+ $ gem install chimera
90
+
91
+ == LICENSE:
92
+
93
+ (The MIT License)
94
+
95
+ Copyright (c) 2010 FIXME full name
96
+
97
+ Permission is hereby granted, free of charge, to any person obtaining
98
+ a copy of this software and associated documentation files (the
99
+ 'Software'), to deal in the Software without restriction, including
100
+ without limitation the rights to use, copy, modify, merge, publish,
101
+ distribute, sublicense, and/or sell copies of the Software, and to
102
+ permit persons to whom the Software is furnished to do so, subject to
103
+ the following conditions:
104
+
105
+ The above copyright notice and this permission notice shall be
106
+ included in all copies or substantial portions of the Software.
107
+
108
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
109
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
110
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
111
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
112
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
113
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
114
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,30 @@
1
+ require 'rubygems'
2
+ gem 'hoe', '>= 2.1.0'
3
+ require 'hoe'
4
+ require 'fileutils'
5
+ require './lib/chimera'
6
+
7
+ Hoe.plugin :newgem
8
+ # Hoe.plugin :website
9
+ # Hoe.plugin :cucumberfeatures
10
+
11
+ # Generate all the Rake tasks
12
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
13
+ $hoe = Hoe.spec 'chimera' do
14
+ self.developer 'Ben Myles', 'ben.myles@gmail.com'
15
+ self.post_install_message = 'PostInstall.txt' # TODO remove if post-install message not required
16
+ self.rubyforge_name = self.name # TODO this is default value
17
+ # self.extra_deps = [['activesupport','>= 2.0.2']]
18
+ self.extra_deps = [["activesupport","= 3.0.0.beta"],
19
+ ["uuidtools","= 2.1.1"],
20
+ ["activemodel",'= 3.0.0.beta'],
21
+ ["brianmario-yajl-ruby","= 0.6.3"],
22
+ ["fast-stemmer", "= 1.0.0"]]
23
+ end
24
+
25
+ require 'newgem/tasks'
26
+ Dir['tasks/**/*.rake'].each { |t| load t }
27
+
28
+ # TODO - want other tests/tasks run by default? Add them to the list
29
+ # remove_task :default
30
+ # task :default => [:spec, :features]
@@ -0,0 +1,11 @@
1
+ require 'lib/chimera'
2
+ Chimera.config_path = "doc/examples/config.yml"
3
+ require 'test/models'
4
+ c = Car.new
5
+ c.make = "Nissan"
6
+ c.model = "RX7"
7
+ c.year = 2010
8
+ c.sku = 1001
9
+ c.comments = "really fast car. it's purple too!"
10
+ c.id = Car.new_uuid
11
+ c.save
@@ -0,0 +1,16 @@
1
+ :default:
2
+ :redis:
3
+ :host: 127.0.0.1
4
+ :port: 6379
5
+ :db: 0
6
+ :riak_raw:
7
+ :host: 127.0.0.1
8
+ :port: 8098
9
+ :user:
10
+ :redis:
11
+ :host: 127.0.0.1
12
+ :port: 5379
13
+ :db: 0
14
+ :riak_raw:
15
+ :host: 127.0.0.1
16
+ :port: 7098
@@ -0,0 +1,132 @@
1
+ # Redis configuration file example
2
+
3
+ # By default Redis does not run as a daemon. Use 'yes' if you need it.
4
+ # Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
5
+ daemonize no
6
+
7
+ # When run as a daemon, Redis write a pid file in /var/run/redis.pid by default.
8
+ # You can specify a custom pid file location here.
9
+ pidfile /tmp/redis6379.pid
10
+
11
+ # Accept connections on the specified port, default is 6379
12
+ port 6379
13
+
14
+ # If you want you can bind a single interface, if the bind option is not
15
+ # specified all the interfaces will listen for connections.
16
+ #
17
+ # bind 127.0.0.1
18
+
19
+ # Close the connection after a client is idle for N seconds (0 to disable)
20
+ timeout 300
21
+
22
+ # Save the DB on disk:
23
+ #
24
+ # save <seconds> <changes>
25
+ #
26
+ # Will save the DB if both the given number of seconds and the given
27
+ # number of write operations against the DB occurred.
28
+ #
29
+ # In the example below the behaviour will be to save:
30
+ # after 900 sec (15 min) if at least 1 key changed
31
+ # after 300 sec (5 min) if at least 10 keys changed
32
+ # after 60 sec if at least 10000 keys changed
33
+ save 900 1
34
+ save 300 10
35
+ save 60 10000
36
+
37
+ # The filename where to dump the DB
38
+ dbfilename /tmp/redis6379.rdb
39
+
40
+ # For default save/load DB in/from the working directory
41
+ # Note that you must specify a directory not a file name.
42
+ dir ./
43
+
44
+ # Set server verbosity to 'debug'
45
+ # it can be one of:
46
+ # debug (a lot of information, useful for development/testing)
47
+ # notice (moderately verbose, what you want in production probably)
48
+ # warning (only very important / critical messages are logged)
49
+ loglevel debug
50
+
51
+ # Specify the log file name. Also 'stdout' can be used to force
52
+ # the demon to log on the standard output. Note that if you use standard
53
+ # output for logging but daemonize, logs will be sent to /dev/null
54
+ logfile stdout
55
+
56
+ # Set the number of databases. The default database is DB 0, you can select
57
+ # a different one on a per-connection basis using SELECT <dbid> where
58
+ # dbid is a number between 0 and 'databases'-1
59
+ databases 256
60
+
61
+ ################################# REPLICATION #################################
62
+
63
+ # Master-Slave replication. Use slaveof to make a Redis instance a copy of
64
+ # another Redis server. Note that the configuration is local to the slave
65
+ # so for example it is possible to configure the slave to save the DB with a
66
+ # different interval, or to listen to another port, and so on.
67
+
68
+ # slaveof <masterip> <masterport>
69
+
70
+ ################################## SECURITY ###################################
71
+
72
+ # Require clients to issue AUTH <PASSWORD> before processing any other
73
+ # commands. This might be useful in environments in which you do not trust
74
+ # others with access to the host running redis-server.
75
+ #
76
+ # This should stay commented out for backward compatibility and because most
77
+ # people do not need auth (e.g. they run their own servers).
78
+
79
+ # requirepass foobared
80
+
81
+ ################################### LIMITS ####################################
82
+
83
+ # Set the max number of connected clients at the same time. By default there
84
+ # is no limit, and it's up to the number of file descriptors the Redis process
85
+ # is able to open. The special value '0' means no limts.
86
+ # Once the limit is reached Redis will close all the new connections sending
87
+ # an error 'max number of clients reached'.
88
+
89
+ # maxclients 128
90
+
91
+ # Don't use more memory than the specified amount of bytes.
92
+ # When the memory limit is reached Redis will try to remove keys with an
93
+ # EXPIRE set. It will try to start freeing keys that are going to expire
94
+ # in little time and preserve keys with a longer time to live.
95
+ # Redis will also try to remove objects from free lists if possible.
96
+ #
97
+ # If all this fails, Redis will start to reply with errors to commands
98
+ # that will use more memory, like SET, LPUSH, and so on, and will continue
99
+ # to reply to most read-only commands like GET.
100
+ #
101
+ # WARNING: maxmemory can be a good idea mainly if you want to use Redis as a
102
+ # 'state' server or cache, not as a real DB. When Redis is used as a real
103
+ # database the memory usage will grow over the weeks, it will be obvious if
104
+ # it is going to use too much memory in the long run, and you'll have the time
105
+ # to upgrade. With maxmemory after the limit is reached you'll start to get
106
+ # errors for write operations, and this may even lead to DB inconsistency.
107
+
108
+ # maxmemory <bytes>
109
+
110
+ ############################### ADVANCED CONFIG ###############################
111
+
112
+ # Glue small output buffers together in order to send small replies in a
113
+ # single TCP packet. Uses a bit more CPU but most of the times it is a win
114
+ # in terms of number of queries per second. Use 'yes' if unsure.
115
+ glueoutputbuf yes
116
+
117
+ # Use object sharing. Can save a lot of memory if you have many common
118
+ # string in your dataset, but performs lookups against the shared objects
119
+ # pool so it uses more CPU and can be a bit slower. Usually it's a good
120
+ # idea.
121
+ #
122
+ # When object sharing is enabled (shareobjects yes) you can use
123
+ # shareobjectspoolsize to control the size of the pool used in order to try
124
+ # object sharing. A bigger pool size will lead to better sharing capabilities.
125
+ # In general you want this value to be at least the double of the number of
126
+ # very common strings you have in your dataset.
127
+ #
128
+ # WARNING: object sharing is experimental, don't enable this feature
129
+ # in production before of Redis 1.0-stable. Still please try this feature in
130
+ # your development environment so that we can test it better.
131
+ shareobjects no
132
+ shareobjectspoolsize 1024
@@ -0,0 +1,33 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ gem 'uuidtools','= 2.1.1'
5
+ gem 'activemodel','= 3.0.0.beta'
6
+ gem "brianmario-yajl-ruby", "= 0.6.3"
7
+ gem "fast-stemmer", "= 1.0.0"
8
+
9
+ require 'fast_stemmer'
10
+
11
+ require 'digest/sha1'
12
+ require 'uuidtools'
13
+ require 'yajl'
14
+ require 'yaml'
15
+ require 'active_model'
16
+
17
+ require 'redis'
18
+ require 'typhoeus'
19
+ require 'riak_raw'
20
+
21
+ require "chimera/error"
22
+ require "chimera/attributes"
23
+ require "chimera/indexes"
24
+ require "chimera/geo_indexes"
25
+ require "chimera/associations"
26
+ require "chimera/redis_objects"
27
+ require "chimera/finders"
28
+ require "chimera/persistence"
29
+ require "chimera/base"
30
+
31
+ module Chimera
32
+ VERSION = '0.0.1'
33
+ end
@@ -0,0 +1,146 @@
1
+ module Chimera
2
+ module Associations
3
+ def self.included(base)
4
+ base.send :extend, ClassMethods
5
+ base.send :include, InstanceMethods
6
+ end
7
+
8
+ module ClassMethods
9
+ def defined_associations
10
+ @defined_associations || {}
11
+ end
12
+
13
+ # association :friends, User
14
+ def association(name, class_sym)
15
+ @defined_associations ||= {}
16
+ @defined_associations[name.to_sym] = class_sym
17
+ define_method("#{name}") do
18
+ @associations ||= {}
19
+ @associations[name] ||= Chimera::AssociationProxies::Association.new(self,name,class_sym)
20
+ end
21
+ end
22
+ end # ClassMethods
23
+
24
+ module InstanceMethods
25
+ def destroy_associations
26
+ (@associations || {}).each do |name, association|
27
+ association.destroy
28
+ end
29
+ end
30
+
31
+ def association_memberships
32
+ @association_memberships ||= Chimera::AssociationProxies::AssociationMemberships.new(self)
33
+ end
34
+ end # InstanceMethods
35
+ end
36
+
37
+ module AssociationProxies
38
+ class AssociationMemberships
39
+ attr_accessor :model
40
+
41
+ def initialize(_model)
42
+ @model = _model
43
+ end
44
+
45
+ def key
46
+ "#{model.class.to_s}::AssociationProxies::AssociationMemberships::#{model.id}"
47
+ end
48
+
49
+ def add(assoc_key)
50
+ self.model.class.connection(:redis).lpush(self.key, assoc_key)
51
+ end
52
+
53
+ def remove(assoc_key)
54
+ self.model.class.connection(:redis).lrem(self.key, 0, assoc_key)
55
+ end
56
+
57
+ def destroy
58
+ remove_from_all_associations
59
+ self.model.class.connection(:redis).del(self.key)
60
+ end
61
+
62
+ def remove_from_all_associations
63
+ self.each_association { |assoc| assoc.remove(self.model) }
64
+ end
65
+
66
+ def each_association
67
+ llen = self.model.class.connection(:redis).llen(self.key)
68
+ 0.upto(llen-1) do |i|
69
+ assoc_key = self.model.class.connection(:redis).lindex(self.key, i)
70
+ yield Chimera::AssociationProxies::Association.find(assoc_key)
71
+ end
72
+ true
73
+ end
74
+
75
+ def all_associations
76
+ all = []; self.each_association { |ass| all << ass }; all
77
+ end
78
+ end
79
+
80
+ class Association
81
+ attr_accessor :model, :name, :klass
82
+
83
+ def self.find(assoc_key)
84
+ parts = assoc_key.split("::")
85
+ model_klass = parts[0]
86
+ name = parts[3]
87
+ assoc_klass = parts[4]
88
+ model_id = parts[5]
89
+ self.new(eval(model_klass).find(model_id), name, assoc_klass.to_sym)
90
+ end
91
+
92
+ def initialize(_model, _name, class_sym)
93
+ @model = _model
94
+ @name = _name
95
+ @klass = eval(class_sym.to_s.camelize)
96
+ raise(Chimera::Error::MissingId) unless model.id
97
+ end
98
+
99
+ def key
100
+ "#{model.class.to_s}::AssociationProxies::Association::#{name}::#{klass.to_s}::#{model.id}"
101
+ end
102
+
103
+ def <<(obj)
104
+ raise(Chimera::Error::AssociationClassMismatch) unless obj.class.to_s == self.klass.to_s
105
+ self.model.class.connection(:redis).lpush(self.key, obj.id)
106
+ obj.association_memberships.add(self.key)
107
+ true
108
+ end
109
+
110
+ def remove(obj)
111
+ raise(Chimera::Error::AssociationClassMismatch) unless obj.class.to_s == self.klass.to_s
112
+ self.model.class.connection(:redis).lrem(self.key, 0, obj.id)
113
+ obj.association_memberships.remove(self.key)
114
+ true
115
+ end
116
+
117
+ def size
118
+ self.model.class.connection(:redis).llen(self.key)
119
+ end
120
+
121
+ def each
122
+ llen = self.model.class.connection(:redis).llen(self.key)
123
+ curr = 0
124
+ while(curr < llen)
125
+ keys = self.model.class.connection(:redis).lrange(self.key, curr, curr+9).compact
126
+ self.klass.find_many(keys).each { |obj| yield(obj) }
127
+ curr += 10
128
+ end
129
+ true
130
+ end
131
+
132
+ def all
133
+ found = []; self.each { |o| found << o }; found
134
+ end
135
+
136
+ def destroy(delete_associated=true)
137
+ if delete_associated == true
138
+ self.each { |obj| obj.destroy }
139
+ else
140
+ self.each { |obj| obj.association_memberships.remove(self.key) }
141
+ end
142
+ self.model.class.connection(:redis).del(self.key)
143
+ end
144
+ end # Association
145
+ end # AssociationProxies
146
+ end