friendly_postgres 0.4.3
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.
- data/.document +2 -0
- data/.gitignore +26 -0
- data/APACHE-LICENSE +202 -0
- data/CHANGELOG.md +19 -0
- data/CONTRIBUTORS.md +7 -0
- data/LICENSE +20 -0
- data/README.md +265 -0
- data/Rakefile +68 -0
- data/TODO.md +5 -0
- data/VERSION +1 -0
- data/examples/friendly.yml +7 -0
- data/friendly.gemspec +232 -0
- data/lib/friendly.rb +54 -0
- data/lib/friendly/associations.rb +7 -0
- data/lib/friendly/associations/association.rb +34 -0
- data/lib/friendly/associations/set.rb +37 -0
- data/lib/friendly/attribute.rb +91 -0
- data/lib/friendly/boolean.rb +10 -0
- data/lib/friendly/cache.rb +24 -0
- data/lib/friendly/cache/by_id.rb +33 -0
- data/lib/friendly/config.rb +5 -0
- data/lib/friendly/data_store.rb +72 -0
- data/lib/friendly/document.rb +257 -0
- data/lib/friendly/document_table.rb +56 -0
- data/lib/friendly/index.rb +73 -0
- data/lib/friendly/memcached.rb +48 -0
- data/lib/friendly/named_scope.rb +17 -0
- data/lib/friendly/newrelic.rb +6 -0
- data/lib/friendly/query.rb +42 -0
- data/lib/friendly/scope.rb +100 -0
- data/lib/friendly/scope_proxy.rb +45 -0
- data/lib/friendly/sequel_monkey_patches.rb +34 -0
- data/lib/friendly/storage.rb +31 -0
- data/lib/friendly/storage_factory.rb +24 -0
- data/lib/friendly/storage_proxy.rb +103 -0
- data/lib/friendly/table.rb +15 -0
- data/lib/friendly/table_creator.rb +48 -0
- data/lib/friendly/time.rb +14 -0
- data/lib/friendly/translator.rb +32 -0
- data/lib/friendly/uuid.rb +148 -0
- data/rails/init.rb +3 -0
- data/spec/config.yml.example +7 -0
- data/spec/fakes/data_store_fake.rb +29 -0
- data/spec/fakes/database_fake.rb +12 -0
- data/spec/fakes/dataset_fake.rb +28 -0
- data/spec/fakes/document.rb +18 -0
- data/spec/fakes/serializer_fake.rb +12 -0
- data/spec/fakes/time_fake.rb +12 -0
- data/spec/integration/ad_hoc_scopes_spec.rb +42 -0
- data/spec/integration/basic_object_lifecycle_spec.rb +114 -0
- data/spec/integration/batch_insertion_spec.rb +29 -0
- data/spec/integration/convenience_api_spec.rb +25 -0
- data/spec/integration/count_spec.rb +12 -0
- data/spec/integration/default_value_spec.rb +15 -0
- data/spec/integration/find_via_cache_spec.rb +101 -0
- data/spec/integration/finder_spec.rb +64 -0
- data/spec/integration/has_many_spec.rb +18 -0
- data/spec/integration/index_spec.rb +57 -0
- data/spec/integration/named_scope_spec.rb +34 -0
- data/spec/integration/pagination_spec.rb +63 -0
- data/spec/integration/scope_chaining_spec.rb +22 -0
- data/spec/integration/table_creator_spec.rb +64 -0
- data/spec/integration/write_through_cache_spec.rb +53 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +103 -0
- data/spec/unit/associations/association_spec.rb +57 -0
- data/spec/unit/associations/set_spec.rb +43 -0
- data/spec/unit/attribute_spec.rb +105 -0
- data/spec/unit/cache_by_id_spec.rb +102 -0
- data/spec/unit/cache_spec.rb +21 -0
- data/spec/unit/config_spec.rb +4 -0
- data/spec/unit/data_store_spec.rb +188 -0
- data/spec/unit/document_spec.rb +358 -0
- data/spec/unit/document_table_spec.rb +126 -0
- data/spec/unit/friendly_spec.rb +25 -0
- data/spec/unit/index_spec.rb +196 -0
- data/spec/unit/memcached_spec.rb +114 -0
- data/spec/unit/named_scope_spec.rb +16 -0
- data/spec/unit/query_spec.rb +104 -0
- data/spec/unit/scope_proxy_spec.rb +44 -0
- data/spec/unit/scope_spec.rb +113 -0
- data/spec/unit/storage_factory_spec.rb +59 -0
- data/spec/unit/storage_proxy_spec.rb +218 -0
- data/spec/unit/translator_spec.rb +96 -0
- data/website/index.html +210 -0
- data/website/scripts/clipboard.swf +0 -0
- data/website/scripts/shBrushAS3.js +61 -0
- data/website/scripts/shBrushBash.js +66 -0
- data/website/scripts/shBrushCSharp.js +67 -0
- data/website/scripts/shBrushColdFusion.js +102 -0
- data/website/scripts/shBrushCpp.js +99 -0
- data/website/scripts/shBrushCss.js +93 -0
- data/website/scripts/shBrushDelphi.js +57 -0
- data/website/scripts/shBrushDiff.js +43 -0
- data/website/scripts/shBrushErlang.js +54 -0
- data/website/scripts/shBrushGroovy.js +69 -0
- data/website/scripts/shBrushJScript.js +52 -0
- data/website/scripts/shBrushJava.js +59 -0
- data/website/scripts/shBrushJavaFX.js +60 -0
- data/website/scripts/shBrushPerl.js +74 -0
- data/website/scripts/shBrushPhp.js +91 -0
- data/website/scripts/shBrushPlain.js +35 -0
- data/website/scripts/shBrushPowerShell.js +76 -0
- data/website/scripts/shBrushPython.js +66 -0
- data/website/scripts/shBrushRuby.js +57 -0
- data/website/scripts/shBrushScala.js +53 -0
- data/website/scripts/shBrushSql.js +68 -0
- data/website/scripts/shBrushVb.js +58 -0
- data/website/scripts/shBrushXml.js +71 -0
- data/website/scripts/shCore.js +30 -0
- data/website/scripts/shLegacy.js +30 -0
- data/website/styles/friendly.css +103 -0
- data/website/styles/help.png +0 -0
- data/website/styles/ie.css +35 -0
- data/website/styles/magnifier.png +0 -0
- data/website/styles/page_white_code.png +0 -0
- data/website/styles/page_white_copy.png +0 -0
- data/website/styles/print.css +29 -0
- data/website/styles/printer.png +0 -0
- data/website/styles/screen.css +257 -0
- data/website/styles/shCore.css +330 -0
- data/website/styles/shThemeDefault.css +173 -0
- data/website/styles/shThemeDjango.css +176 -0
- data/website/styles/shThemeEclipse.css +190 -0
- data/website/styles/shThemeEmacs.css +175 -0
- data/website/styles/shThemeFadeToGrey.css +177 -0
- data/website/styles/shThemeMidnight.css +175 -0
- data/website/styles/shThemeRDark.css +175 -0
- metadata +302 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
module Friendly
|
|
2
|
+
class Attribute
|
|
3
|
+
class << self
|
|
4
|
+
def register_type(type, sql_type, &block)
|
|
5
|
+
sql_types[type.name] = sql_type
|
|
6
|
+
converters[type] = block
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def deregister_type(type)
|
|
10
|
+
sql_types.delete(type.name)
|
|
11
|
+
converters.delete(type)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def sql_type(type)
|
|
15
|
+
sql_types[type.name]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def sql_types
|
|
19
|
+
@sql_types ||= {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def converters
|
|
23
|
+
@converters ||= {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def custom_type?(klass)
|
|
27
|
+
!sql_type(klass).nil?
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
converters[Integer] = lambda { |s| s.to_i }
|
|
32
|
+
converters[String] = lambda { |s| s.to_s }
|
|
33
|
+
|
|
34
|
+
attr_reader :klass, :name, :type, :default_value
|
|
35
|
+
|
|
36
|
+
def initialize(klass, name, type, options = {})
|
|
37
|
+
@klass = klass
|
|
38
|
+
@name = name
|
|
39
|
+
@type = type
|
|
40
|
+
@default_value = options[:default]
|
|
41
|
+
build_accessors
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def typecast(value)
|
|
45
|
+
!type || value.is_a?(type) ? value : convert(value)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def convert(value)
|
|
49
|
+
assert_converter_exists(value)
|
|
50
|
+
converters[type].call(value)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def default
|
|
54
|
+
if !default_value.nil?
|
|
55
|
+
default_value
|
|
56
|
+
elsif type.respond_to?(:new)
|
|
57
|
+
type.new
|
|
58
|
+
else
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
protected
|
|
64
|
+
def build_accessors
|
|
65
|
+
n = name
|
|
66
|
+
klass.class_eval do
|
|
67
|
+
eval <<-__END__
|
|
68
|
+
def #{n}=(value)
|
|
69
|
+
@#{n} = self.class.attributes[:#{n}].typecast(value)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def #{n}
|
|
73
|
+
@#{n} ||= self.class.attributes[:#{n}].default
|
|
74
|
+
end
|
|
75
|
+
__END__
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def assert_converter_exists(value)
|
|
80
|
+
unless converters.has_key?(type)
|
|
81
|
+
msg = "Can't convert #{value} to #{type}.
|
|
82
|
+
Add a custom converter to Friendly::Attribute::CONVERTERS."
|
|
83
|
+
raise NoConverterExists, msg
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def converters
|
|
88
|
+
self.class.converters
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require 'friendly/storage'
|
|
2
|
+
|
|
3
|
+
module Friendly
|
|
4
|
+
class Cache < Storage
|
|
5
|
+
class << self
|
|
6
|
+
def cache_for(klass, fields, options)
|
|
7
|
+
unless fields == [:id]
|
|
8
|
+
raise NotSupported, "Caching is only possible by id at the moment."
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
ByID.new(klass, fields, options)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_reader :klass, :fields, :cache, :version
|
|
16
|
+
|
|
17
|
+
def initialize(klass, fields, options = {}, cache = Friendly.cache)
|
|
18
|
+
@klass = klass
|
|
19
|
+
@fields = fields
|
|
20
|
+
@cache = cache
|
|
21
|
+
@version = options[:version] || 0
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Friendly
|
|
2
|
+
class Cache
|
|
3
|
+
class ByID < Cache
|
|
4
|
+
def store(document)
|
|
5
|
+
cache.set(cache_key(document.id), document)
|
|
6
|
+
end
|
|
7
|
+
alias_method :create, :store
|
|
8
|
+
alias_method :update, :store
|
|
9
|
+
|
|
10
|
+
def destroy(document)
|
|
11
|
+
cache.delete(cache_key(document.id))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def first(query, &block)
|
|
15
|
+
cache.get(cache_key(query.conditions[:id]), &block)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def all(query, &block)
|
|
19
|
+
keys = query.conditions[:id].map { |k| cache_key(k) }
|
|
20
|
+
cache.multiget(keys, &block).values
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def satisfies?(query)
|
|
24
|
+
query.conditions.keys == [:id]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
protected
|
|
28
|
+
def cache_key(id)
|
|
29
|
+
[klass.name, version, id.to_guid].join("/")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module Friendly
|
|
2
|
+
class DataStore
|
|
3
|
+
attr_reader :database
|
|
4
|
+
|
|
5
|
+
def initialize(database)
|
|
6
|
+
@database = database
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def insert(persistable, attributes)
|
|
10
|
+
batch? ? batch_insert(persistable, attributes) :
|
|
11
|
+
immediate_insert(persistable, attributes)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def all(persistable, query)
|
|
15
|
+
filtered = dataset(persistable).where(query.conditions)
|
|
16
|
+
if query.limit || query.offset
|
|
17
|
+
filtered = filtered.limit(query.limit, query.offset)
|
|
18
|
+
end
|
|
19
|
+
filtered = filtered.order(query.order) if query.order
|
|
20
|
+
filtered.map
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def first(persistable, query)
|
|
24
|
+
dataset(persistable).first(query.conditions)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def update(persistable, id, attributes)
|
|
28
|
+
dataset(persistable).where(:id => id).update(attributes)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def delete(persistable, id)
|
|
32
|
+
dataset(persistable).where(:id => id).delete
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def count(persistable, query)
|
|
36
|
+
dataset(persistable).where(query.conditions).count
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def start_batch
|
|
40
|
+
Thread.current[:friendly_batch] = Hash.new { |h, k| h[k] = [] }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def reset_batch
|
|
44
|
+
Thread.current[:friendly_batch] = nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def flush_batch
|
|
48
|
+
batch = Thread.current[:friendly_batch]
|
|
49
|
+
batch.keys.each do |k|
|
|
50
|
+
database.from(k).multi_insert(batch[k], :commit_every => 1000)
|
|
51
|
+
end
|
|
52
|
+
reset_batch
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
protected
|
|
56
|
+
def dataset(persistable)
|
|
57
|
+
database.from(persistable.table_name)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def immediate_insert(persistable, attributes)
|
|
61
|
+
dataset(persistable).insert(attributes)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def batch_insert(persistable, attributes)
|
|
65
|
+
Thread.current[:friendly_batch][persistable.table_name] << attributes
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def batch?
|
|
69
|
+
Thread.current[:friendly_batch]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
require 'active_support/inflector'
|
|
2
|
+
require 'friendly/associations'
|
|
3
|
+
|
|
4
|
+
module Friendly
|
|
5
|
+
module Document
|
|
6
|
+
class << self
|
|
7
|
+
attr_writer :documents
|
|
8
|
+
|
|
9
|
+
def included(klass)
|
|
10
|
+
documents << klass
|
|
11
|
+
klass.class_eval do
|
|
12
|
+
extend ClassMethods
|
|
13
|
+
attribute :id, UUID
|
|
14
|
+
attribute :created_at, Time
|
|
15
|
+
attribute :updated_at, Time
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def documents
|
|
20
|
+
@documents ||= []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def create_tables!
|
|
24
|
+
documents.each { |d| d.create_tables! }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module ClassMethods
|
|
29
|
+
attr_writer :storage_proxy, :query_klass,
|
|
30
|
+
:table_name, :collection_klass,
|
|
31
|
+
:scope_proxy, :association_set
|
|
32
|
+
|
|
33
|
+
def create_tables!
|
|
34
|
+
storage_proxy.create_tables!
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def attribute(name, type = nil, options = {})
|
|
38
|
+
attributes[name] = Attribute.new(self, name, type, options)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def storage_proxy
|
|
42
|
+
@storage_proxy ||= StorageProxy.new(self)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def query_klass
|
|
46
|
+
@query_klass ||= Query
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def collection_klass
|
|
50
|
+
@collection_klass ||= WillPaginate::Collection
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def indexes(*args)
|
|
54
|
+
storage_proxy.add(args)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def caches_by(*fields)
|
|
58
|
+
options = fields.last.is_a?(Hash) ? fields.pop : {}
|
|
59
|
+
storage_proxy.cache(fields, options)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def attributes
|
|
63
|
+
@attributes ||= {}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def first(query)
|
|
67
|
+
storage_proxy.first(query(query))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def all(query)
|
|
71
|
+
storage_proxy.all(query(query))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def find(id)
|
|
75
|
+
doc = first(:id => id)
|
|
76
|
+
raise RecordNotFound, "Couldn't find #{name}/#{id}" if doc.nil?
|
|
77
|
+
doc
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def count(conditions)
|
|
81
|
+
storage_proxy.count(query(conditions))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def paginate(conditions)
|
|
85
|
+
query = query(conditions)
|
|
86
|
+
count = count(query)
|
|
87
|
+
collection = collection_klass.new(query.page, query.per_page, count)
|
|
88
|
+
collection.replace(all(query))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def create(attributes = {})
|
|
92
|
+
doc = new(attributes)
|
|
93
|
+
doc.save
|
|
94
|
+
doc
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def table_name
|
|
98
|
+
@table_name ||= name.pluralize.underscore
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def scope_proxy
|
|
102
|
+
@scope_proxy ||= ScopeProxy.new(self)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Add a named scope to this Document.
|
|
106
|
+
#
|
|
107
|
+
# e.g.
|
|
108
|
+
#
|
|
109
|
+
# class Post
|
|
110
|
+
# indexes :created_at
|
|
111
|
+
# named_scope :recent, :order! => :created_at.desc
|
|
112
|
+
# end
|
|
113
|
+
#
|
|
114
|
+
# Then, you can access the recent posts with:
|
|
115
|
+
#
|
|
116
|
+
# Post.recent.all
|
|
117
|
+
# ...or...
|
|
118
|
+
# Post.recent.first
|
|
119
|
+
#
|
|
120
|
+
# Both #all and #first also take additional parameters:
|
|
121
|
+
#
|
|
122
|
+
# Post.recent.all(:author_id => @author.id)
|
|
123
|
+
#
|
|
124
|
+
# Scopes are also chainable. See the README or Friendly::Scope docs for details.
|
|
125
|
+
#
|
|
126
|
+
# @param [Symbol] name the name of the scope.
|
|
127
|
+
# @param [Hash] parameters the query that this named scope will perform.
|
|
128
|
+
#
|
|
129
|
+
def named_scope(name, parameters)
|
|
130
|
+
scope_proxy.add_named(name, parameters)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Returns boolean based on whether the Document has a scope by a particular name.
|
|
134
|
+
#
|
|
135
|
+
# @param [Symbol] name The name of the scope in question.
|
|
136
|
+
#
|
|
137
|
+
def has_named_scope?(name)
|
|
138
|
+
scope_proxy.has_named_scope?(name)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Create an ad hoc scope on this Document.
|
|
142
|
+
#
|
|
143
|
+
# e.g.
|
|
144
|
+
#
|
|
145
|
+
# scope = Post.scope(:order! => :created_at)
|
|
146
|
+
# scope.all # => [#<Post>, #<Post>]
|
|
147
|
+
#
|
|
148
|
+
# @param [Hash] parameters the query parameters to create the scope with.
|
|
149
|
+
#
|
|
150
|
+
def scope(parameters)
|
|
151
|
+
scope_proxy.ad_hoc(parameters)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def association_set
|
|
155
|
+
@association_set ||= Associations::Set.new(self)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Add a has_many association.
|
|
159
|
+
#
|
|
160
|
+
# e.g.
|
|
161
|
+
#
|
|
162
|
+
# class Post
|
|
163
|
+
# attribute :user_id, Friendly::UUID
|
|
164
|
+
# indexes :user_id
|
|
165
|
+
# end
|
|
166
|
+
#
|
|
167
|
+
# class User
|
|
168
|
+
# has_many :posts
|
|
169
|
+
# end
|
|
170
|
+
#
|
|
171
|
+
# @user = User.create
|
|
172
|
+
# @post = @user.posts.create
|
|
173
|
+
# @user.posts.all == [@post] # => true
|
|
174
|
+
#
|
|
175
|
+
# _Note: Make sure that the target model is indexed on the foreign key. If it isn't, querying the association will raise Friendly::MissingIndex._
|
|
176
|
+
#
|
|
177
|
+
# Friendly defaults the foreign key to class_name_id just like ActiveRecord.
|
|
178
|
+
# It also converts the name of the association to the name of the target class just like ActiveRecord does.
|
|
179
|
+
#
|
|
180
|
+
# The biggest difference in semantics between Friendly's has_many and active_record's is that Friendly's just returns a Friendly::Scope object. If you want all the associated objects, you have to call #all to get them. You can also use any other Friendly::Scope method.
|
|
181
|
+
#
|
|
182
|
+
# @param [Symbol] name The name of the association and plural name of the target class.
|
|
183
|
+
# @option options [String] :class_name The name of the target class of this association if it is different than the name would imply.
|
|
184
|
+
# @option options [Symbol] :foreign_key Override the foreign key.
|
|
185
|
+
#
|
|
186
|
+
def has_many(name, options = {})
|
|
187
|
+
association_set.add(name, options)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
protected
|
|
191
|
+
def query(conditions)
|
|
192
|
+
conditions.is_a?(Query) ? conditions : query_klass.new(conditions)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def initialize(opts = {})
|
|
197
|
+
self.attributes = opts
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def attributes=(attrs)
|
|
201
|
+
assert_no_duplicate_keys(attrs)
|
|
202
|
+
attrs.each { |name, value| send("#{name}=", value) }
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def save
|
|
206
|
+
new_record? ? storage_proxy.create(self) : storage_proxy.update(self)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def update_attributes(attributes)
|
|
210
|
+
self.attributes = attributes
|
|
211
|
+
save
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def destroy
|
|
215
|
+
storage_proxy.destroy(self)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def to_hash
|
|
219
|
+
Hash[*self.class.attributes.keys.map { |n| [n, send(n)] }.flatten]
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def table_name
|
|
223
|
+
self.class.table_name
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def new_record?
|
|
227
|
+
new_record
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def new_record
|
|
231
|
+
@new_record = true if @new_record.nil?
|
|
232
|
+
@new_record
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def new_record=(value)
|
|
236
|
+
@new_record = value
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def storage_proxy
|
|
240
|
+
self.class.storage_proxy
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def ==(comparison_object)
|
|
244
|
+
comparison_object.equal?(self) ||
|
|
245
|
+
(comparison_object.is_a?(self.class) &&
|
|
246
|
+
!comparison_object.new_record? &&
|
|
247
|
+
comparison_object.id == id)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
protected
|
|
251
|
+
def assert_no_duplicate_keys(hash)
|
|
252
|
+
if hash.keys.map { |k| k.to_s }.uniq.length < hash.keys.length
|
|
253
|
+
raise ArgumentError, "Duplicate keys: #{hash.inspect}"
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|