lore 0.4.8 → 0.9.2
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/Manifest.txt +16 -7
- data/README.rdoc +91 -0
- data/benchmark/benchmark.sql +11 -0
- data/benchmark/results.txt +28 -0
- data/benchmark/select.rb +352 -0
- data/lib/lore.rb +22 -8
- data/lib/lore/adapters/context.rb +64 -0
- data/lib/lore/adapters/postgres-pr.rb +6 -0
- data/lib/lore/adapters/postgres-pr/connection.rb +93 -0
- data/lib/lore/adapters/postgres-pr/result.rb +63 -0
- data/lib/lore/{types.rb → adapters/postgres-pr/types.rb} +36 -0
- data/lib/lore/adapters/postgres.rb +24 -0
- data/lib/lore/adapters/postgres/connection.rb +81 -0
- data/lib/lore/adapters/postgres/result.rb +82 -0
- data/lib/lore/adapters/postgres/types.rb +91 -0
- data/lib/lore/bits.rb +18 -0
- data/lib/lore/cache/abstract_entity_cache.rb +2 -1
- data/lib/lore/cache/cacheable.rb +12 -177
- data/lib/lore/cache/memcache_entity_cache.rb +89 -0
- data/lib/lore/cache/memory_entity_cache.rb +77 -0
- data/lib/lore/cache/mmap_entity_cache.rb +2 -2
- data/lib/lore/cache/mmap_entity_cache_bork.rb +86 -0
- data/lib/lore/clause.rb +107 -35
- data/lib/lore/{exception → exceptions}/ambiguous_attribute.rb +2 -2
- data/lib/lore/{exception → exceptions}/cache_exception.rb +1 -1
- data/lib/lore/exceptions/database_exception.rb +16 -0
- data/lib/lore/{exception/invalid_parameter.rb → exceptions/invalid_field.rb} +7 -4
- data/lib/lore/exceptions/unknown_type.rb +18 -0
- data/lib/lore/exceptions/validation_failure.rb +71 -0
- data/lib/lore/gui/form_generator.rb +109 -60
- data/lib/lore/gui/lore_model_select_field.rb +1 -0
- data/lib/lore/migration.rb +84 -25
- data/lib/lore/model.rb +3 -18
- data/lib/lore/{aspect.rb → model/aspect.rb} +0 -0
- data/lib/lore/model/associations.rb +225 -0
- data/lib/lore/model/attribute_settings.rb +233 -0
- data/lib/lore/model/filters.rb +34 -0
- data/lib/lore/model/mockable.rb +62 -0
- data/lib/lore/{model_factory.rb → model/model_factory.rb} +68 -39
- data/lib/lore/model/model_instance.rb +382 -0
- data/lib/lore/{model_shortcuts.rb → model/model_shortcuts.rb} +7 -0
- data/lib/lore/model/polymorphic.rb +53 -0
- data/lib/lore/model/prepare.rb +97 -0
- data/lib/lore/model/table_accessor.rb +1016 -0
- data/lib/lore/query.rb +71 -0
- data/lib/lore/query_shortcuts.rb +43 -11
- data/lib/lore/strategies/table_delete.rb +115 -0
- data/lib/lore/strategies/table_insert.rb +146 -0
- data/lib/lore/strategies/table_select.rb +299 -0
- data/lib/lore/strategies/table_update.rb +155 -0
- data/lib/lore/validation/parameter_validator.rb +85 -26
- data/lib/lore/validation/type_validator.rb +34 -78
- data/{custom_models.rb → lore-0.9.2.gem} +0 -0
- data/lore.gemspec +26 -17
- data/spec/clause.rb +37 -0
- data/spec/fixtures/blank_models.rb +37 -0
- data/{test/model.rb → spec/fixtures/models.rb} +64 -41
- data/spec/fixtures/polymorphic_models.rb +68 -0
- data/spec/model_associations.rb +86 -0
- data/spec/model_create.rb +47 -0
- data/spec/model_definition.rb +151 -0
- data/spec/model_delete.rb +31 -0
- data/spec/model_inheritance.rb +50 -0
- data/spec/model_polymorphic.rb +85 -0
- data/spec/model_select.rb +101 -0
- data/spec/model_select_eager.rb +42 -0
- data/spec/model_union_select.rb +33 -0
- data/spec/model_update.rb +45 -0
- data/spec/model_validation.rb +20 -0
- data/spec/spec_db.sql +808 -0
- data/spec/spec_env.rb +19 -0
- data/spec/spec_helpers.rb +77 -0
- metadata +93 -82
- data/lib/lore/README.txt +0 -84
- data/lib/lore/behaviours/lockable.rb +0 -55
- data/lib/lore/behaviours/movable.rb +0 -72
- data/lib/lore/behaviours/paginated.rb +0 -31
- data/lib/lore/behaviours/versioned.rb +0 -36
- data/lib/lore/connection.rb +0 -152
- data/lib/lore/exception/invalid_klass_parameters.rb +0 -63
- data/lib/lore/exception/unknown_typecode.rb +0 -19
- data/lib/lore/result.rb +0 -119
- data/lib/lore/symbol.rb +0 -58
- data/lib/lore/table_accessor.rb +0 -1790
- data/lib/lore/table_deleter.rb +0 -116
- data/lib/lore/table_inserter.rb +0 -170
- data/lib/lore/table_instance.rb +0 -389
- data/lib/lore/table_selector.rb +0 -285
- data/lib/lore/table_updater.rb +0 -157
- data/lib/lore/validation.rb +0 -65
- data/lib/lore/validation/message.rb +0 -60
- data/lib/lore/validation/reason.rb +0 -52
- data/lore_test.log +0 -2366
- data/test/README +0 -31
- data/test/custom_models.rb +0 -18
- data/test/env.rb +0 -5
- data/test/prepare.rb +0 -37
- data/test/tc_aspect.rb +0 -58
- data/test/tc_cache.rb +0 -83
- data/test/tc_clause.rb +0 -104
- data/test/tc_deep_inheritance.rb +0 -49
- data/test/tc_factory.rb +0 -57
- data/test/tc_filter.rb +0 -37
- data/test/tc_form.rb +0 -32
- data/test/tc_model.rb +0 -140
- data/test/tc_prepare.rb +0 -44
- data/test/tc_refined_query.rb +0 -88
- data/test/tc_table_accessor.rb +0 -267
- data/test/tc_thread.rb +0 -100
- data/test/test_db.sql +0 -400
- data/test/test_lore.rb +0 -50
|
@@ -6,7 +6,14 @@ module Lore
|
|
|
6
6
|
def html_escape_values_of(*attributes)
|
|
7
7
|
attributes.each { |attrib|
|
|
8
8
|
add_input_filter(attrib) { |a|
|
|
9
|
+
a = a.to_s
|
|
9
10
|
a.gsub("'",''')
|
|
11
|
+
a.gsub("\"",'"')
|
|
12
|
+
}
|
|
13
|
+
add_output_filter(attrib) { |a|
|
|
14
|
+
a = a.to_s
|
|
15
|
+
a.gsub("'",''')
|
|
16
|
+
a.gsub("\"",'"')
|
|
10
17
|
}
|
|
11
18
|
}
|
|
12
19
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
|
|
2
|
+
require('lore')
|
|
3
|
+
|
|
4
|
+
module Lore
|
|
5
|
+
|
|
6
|
+
class Lore::Table_Accessor
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module Polymorphic_Class_Methods
|
|
10
|
+
|
|
11
|
+
def is_polymorphic(key=:concrete_model)
|
|
12
|
+
@polymorphic_attribute = key.to_sym
|
|
13
|
+
@polymorphic_attribute_index = get_fields_flat.index(key.to_sym)
|
|
14
|
+
@is_polymorphic = true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def is_polymorphic?
|
|
18
|
+
(@is_polymorphic == true)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def select_polymorphic(clause=nil, &block)
|
|
22
|
+
base_entities = select(clause, &block)
|
|
23
|
+
base_entities.map { |e|
|
|
24
|
+
cmodel = e.get_concrete_model
|
|
25
|
+
if cmodel then
|
|
26
|
+
e = cmodel.load(get_fields[table_name].first => e.pkey())
|
|
27
|
+
else
|
|
28
|
+
e = false
|
|
29
|
+
end
|
|
30
|
+
e
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def polymorphic_attribute
|
|
35
|
+
@polymorphic_attribute
|
|
36
|
+
end
|
|
37
|
+
def polymorphic_attribute_index
|
|
38
|
+
@polymorphic_attribute_index
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
module Polymorphic_Instance_Methods
|
|
44
|
+
|
|
45
|
+
def get_concrete_model
|
|
46
|
+
concrete_model_name = self.__send__(self.class.polymorphic_attribute)
|
|
47
|
+
return unless concrete_model_name
|
|
48
|
+
eval(concrete_model_name)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
|
|
2
|
+
module Lore
|
|
3
|
+
|
|
4
|
+
module Prepare
|
|
5
|
+
|
|
6
|
+
# Defines prepared statements like
|
|
7
|
+
# The_Model.by_id(id), The_Model.latest_entries(id, amount) etc.
|
|
8
|
+
def define_default_preps
|
|
9
|
+
return
|
|
10
|
+
unless @@prepared_statements[:default_preps] then
|
|
11
|
+
pkey_attrib_name = table_name + '.' << @primary_keys[table_name].first.to_s
|
|
12
|
+
prepare(:_by_id, Lore::Type.integer) { |e|
|
|
13
|
+
e.where(self.__send__(pkey_attrib_name.to_s.split('.')[-1].intern) == Lore::Clause.new('$1'))
|
|
14
|
+
e.limit(1)
|
|
15
|
+
}
|
|
16
|
+
prepare(:_latest, Lore::Type.integer) { |e|
|
|
17
|
+
e.where(true)
|
|
18
|
+
e.order_by(pkey_attrib_name, :desc)
|
|
19
|
+
e.limit(Lore::Clause.new('$1'))
|
|
20
|
+
}
|
|
21
|
+
@@prepared_statements[:default_preps] = true
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Model extension for prepared statements
|
|
28
|
+
module Prepare_Class_Methods
|
|
29
|
+
|
|
30
|
+
@@prepared_statements = Hash.new
|
|
31
|
+
|
|
32
|
+
def by_id(entity_id)
|
|
33
|
+
begin
|
|
34
|
+
return _by_id(entity_id).first
|
|
35
|
+
rescue ::Exception => excep
|
|
36
|
+
if @@prepared_statements[:default_preps] then
|
|
37
|
+
raise excep
|
|
38
|
+
else
|
|
39
|
+
raise ::Exception.new(excep.message + ' (call define_default_preps first?)')
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Prepares a query for execution. This offers four advantages:
|
|
45
|
+
# - The query doesn't have to be interpreted by the DB every time
|
|
46
|
+
# - The query call is available via direct method call.
|
|
47
|
+
# - DB validates against types, thus preventing SQL injection
|
|
48
|
+
# - It doesn't Lore require to compose the query string again. This
|
|
49
|
+
# effects the most significant performance gain (Up to 60% execution
|
|
50
|
+
# time in some benchmarks)
|
|
51
|
+
# Usage:
|
|
52
|
+
#
|
|
53
|
+
# Article.prepare(:by_name_and_date, Lore::Type::Integer, Lore::Type::Date) { |a,fields|
|
|
54
|
+
# a.where((Article.article_id == fields[0] &
|
|
55
|
+
# (Article.date == fields[1]))
|
|
56
|
+
# }
|
|
57
|
+
# Article.by_name_and_date('Merry Christmas', '20081224')
|
|
58
|
+
#
|
|
59
|
+
# From the PostgreSQL 7.4 Manual:
|
|
60
|
+
#
|
|
61
|
+
# "In some situations, the query plan produced by for a prepared statement may be
|
|
62
|
+
# inferior to the plan produced if the statement were submitted and executed normally.
|
|
63
|
+
# This is because when the statement is planned and the planner attempts to determine
|
|
64
|
+
# the optimal query plan, the actual values of any parameters specified in the
|
|
65
|
+
# statement are unavailable. PostgreSQL collects statistics on the distribution of
|
|
66
|
+
# data in the table, and can use constant values in a statement to make guesses about
|
|
67
|
+
# the likely result of executing the statement. Since this data is unavailable when
|
|
68
|
+
# planning prepared statements with parameters, the chosen plan may be suboptimal. To
|
|
69
|
+
# examine the query plan PostgreSQL has chosen for a prepared statement, use
|
|
70
|
+
# EXPLAIN EXECUTE. "
|
|
71
|
+
#
|
|
72
|
+
def prepare(plan_name, *args, &block)
|
|
73
|
+
# {{{
|
|
74
|
+
# log('PREPARE: TRYING CLASS METHOD ' << plan_name.to_s)
|
|
75
|
+
if !@@prepared_statements[plan_name] then
|
|
76
|
+
Table_Select.new(self).prepare(plan_name, args, &block)
|
|
77
|
+
|
|
78
|
+
# log('PREPARE: CREATE CLASS METHOD ' << plan_name.to_s)
|
|
79
|
+
instance_eval("
|
|
80
|
+
def #{plan_name.to_s}(*args)
|
|
81
|
+
execute_prepared(:#{plan_name}, args)
|
|
82
|
+
end")
|
|
83
|
+
@@prepared_statements[plan_name] = true
|
|
84
|
+
# log('PREPARE: CREATED CLASS METHOD ' << plan_name.to_s)
|
|
85
|
+
end
|
|
86
|
+
end # }}}
|
|
87
|
+
|
|
88
|
+
def execute_prepared(plan_name, *args)
|
|
89
|
+
plan_name = "#{table_name.gsub('.','_')}__#{plan_name.to_s}"
|
|
90
|
+
Table_Select.new(self).select_prepared(plan_name, args)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
end
|
|
@@ -0,0 +1,1016 @@
|
|
|
1
|
+
|
|
2
|
+
require('logger')
|
|
3
|
+
require('rubygems')
|
|
4
|
+
require('lore')
|
|
5
|
+
require('lore/clause')
|
|
6
|
+
require('lore/model/aspect')
|
|
7
|
+
require('lore/model/associations')
|
|
8
|
+
require('lore/model/attribute_settings')
|
|
9
|
+
require('lore/model/filters')
|
|
10
|
+
require('lore/query')
|
|
11
|
+
|
|
12
|
+
require('lore/strategies/table_select')
|
|
13
|
+
require('lore/strategies/table_insert')
|
|
14
|
+
require('lore/strategies/table_update')
|
|
15
|
+
require('lore/strategies/table_delete')
|
|
16
|
+
|
|
17
|
+
require('lore/query_shortcuts')
|
|
18
|
+
require('lore/model/model_shortcuts')
|
|
19
|
+
|
|
20
|
+
require('lore/model/model_instance')
|
|
21
|
+
require('lore/model/polymorphic')
|
|
22
|
+
require('lore/model/prepare')
|
|
23
|
+
require('lore/model/mockable')
|
|
24
|
+
require('lore/cache/cacheable')
|
|
25
|
+
|
|
26
|
+
module Lore
|
|
27
|
+
|
|
28
|
+
class Table_Accessor
|
|
29
|
+
include Model_Instance
|
|
30
|
+
extend Aspect
|
|
31
|
+
extend Prepare
|
|
32
|
+
extend Query_Shortcuts
|
|
33
|
+
extend Model_Shortcuts
|
|
34
|
+
extend Polymorphic_Class_Methods
|
|
35
|
+
include Polymorphic_Instance_Methods
|
|
36
|
+
extend Prepare_Class_Methods
|
|
37
|
+
include Prepare
|
|
38
|
+
extend Cache::Cacheable
|
|
39
|
+
|
|
40
|
+
@@logger = Lore.logger
|
|
41
|
+
|
|
42
|
+
def self.log(message, level=:debug)
|
|
43
|
+
@@logger.debug(message)
|
|
44
|
+
end
|
|
45
|
+
def log(message, level=:debug)
|
|
46
|
+
@@logger.debug(message)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Set in load_attribute_fields()
|
|
50
|
+
@__filters__ = false
|
|
51
|
+
@__associations__ = false
|
|
52
|
+
@__attributes__ = false
|
|
53
|
+
|
|
54
|
+
def self.__filters__
|
|
55
|
+
@__filters__
|
|
56
|
+
end
|
|
57
|
+
def self.__associations__
|
|
58
|
+
@__associations__
|
|
59
|
+
end
|
|
60
|
+
def self.__attributes__
|
|
61
|
+
@__attributes__
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.__select_strategy__
|
|
65
|
+
@select_strategy
|
|
66
|
+
end
|
|
67
|
+
def self.__insert_strategy__
|
|
68
|
+
@insert_strategy
|
|
69
|
+
end
|
|
70
|
+
def self.__update_strategy__
|
|
71
|
+
@update_strategy
|
|
72
|
+
end
|
|
73
|
+
def self.__delete_strategy__
|
|
74
|
+
@delete_strategy
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Holds own table name, defaults to self.class if not given.
|
|
78
|
+
# If a schema has been set by
|
|
79
|
+
# table :table_name, :schema_name
|
|
80
|
+
# table_name is schema_name.table_name
|
|
81
|
+
@table_name = String.new
|
|
82
|
+
|
|
83
|
+
@labels = Array.new
|
|
84
|
+
|
|
85
|
+
public
|
|
86
|
+
|
|
87
|
+
##########################################################################
|
|
88
|
+
# Constructor is usually wrapped by e.g. self.load or
|
|
89
|
+
# Model_Instancemarshal_load.
|
|
90
|
+
# Constructor just accepts a value hash, and returns a Table_Accessor
|
|
91
|
+
# instance holding it.
|
|
92
|
+
# Note that this method is operating on a Table_Accessor instance, not
|
|
93
|
+
# on class Table_Accessor itself.
|
|
94
|
+
def initialize(values, joined_models=[], cache=nil)
|
|
95
|
+
# {{{
|
|
96
|
+
# Note:
|
|
97
|
+
# 90% of additional time used on a query compared to
|
|
98
|
+
# plain, unprocessed SQL queries is consumed here - so
|
|
99
|
+
# be efficient!
|
|
100
|
+
|
|
101
|
+
@loaded_from_cache = (cache == :cached)
|
|
102
|
+
@joined_models = joined_models
|
|
103
|
+
|
|
104
|
+
if @loaded_from_cache then
|
|
105
|
+
@attribute_values_flat = values
|
|
106
|
+
else
|
|
107
|
+
@attribute_values_flat = {}
|
|
108
|
+
field_index = 0
|
|
109
|
+
models = [ self.class ]
|
|
110
|
+
models |= joined_models
|
|
111
|
+
models.each { |model|
|
|
112
|
+
tables = model.all_table_names
|
|
113
|
+
fields = model.get_fields_flat
|
|
114
|
+
# Increment over all fields, do not use each_with_index
|
|
115
|
+
fields.each { |field|
|
|
116
|
+
# First value set has precedence
|
|
117
|
+
@attribute_values_flat[field] ||= values[field_index]
|
|
118
|
+
field_index += 1
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
# Applying filter to *all* attribute values, including derived attributes.
|
|
122
|
+
# This way, an output filter can be added in a derived type that does not
|
|
123
|
+
# exist in a base type.
|
|
124
|
+
|
|
125
|
+
return if self.class.output_filters_disabled?
|
|
126
|
+
|
|
127
|
+
output_filters = self.class.__filters__.output_filters
|
|
128
|
+
@attribute_values_flat.each_pair { |attribute, value|
|
|
129
|
+
filter = output_filters[attribute]
|
|
130
|
+
@attribute_values_flat[attribute] = filter.call(value) if filter
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# To be used indirectly via Model.polymorphic_select
|
|
136
|
+
# Parameter 'values' contains values for all fields returned from a
|
|
137
|
+
# polmymorphic select query.
|
|
138
|
+
def self.new_polymorphic(values, joined_models=[])
|
|
139
|
+
# {{{
|
|
140
|
+
# Parameter 'values' contains values for all fields returned from a
|
|
141
|
+
# polmymorphic select query. It thus contains (empty) fields that are
|
|
142
|
+
# not relevant for this model instance.
|
|
143
|
+
# Those have to be filtered out by resolving field indices and their
|
|
144
|
+
# offsets.
|
|
145
|
+
#
|
|
146
|
+
# Model.new expects values in following order:
|
|
147
|
+
#
|
|
148
|
+
# [ own fields, base klass fields, aggregeate klass fields, custom join fields ]
|
|
149
|
+
#
|
|
150
|
+
# But polymorphic selects return:
|
|
151
|
+
#
|
|
152
|
+
# [ polymorphic base klass fields, own fields, other base klass fields ... ]
|
|
153
|
+
#
|
|
154
|
+
# So value array has to be transformed accordingly, which is rather
|
|
155
|
+
# complicated.
|
|
156
|
+
#
|
|
157
|
+
fields = get_fields_flat
|
|
158
|
+
concrete_model_index = 0
|
|
159
|
+
concrete_model_name = values[polymorphic_attribute_index]
|
|
160
|
+
concrete_model = eval(concrete_model_name)
|
|
161
|
+
|
|
162
|
+
# We need to know where to inject values of the polymorphic
|
|
163
|
+
# base model:
|
|
164
|
+
concrete_base_joins = concrete_model.__associations__.joins
|
|
165
|
+
concrete_base_klasses = concrete_model.__associations__.base_klasses
|
|
166
|
+
inject_index = concrete_model.__attributes__.num_own_fields
|
|
167
|
+
# Be sure to iterate just like in
|
|
168
|
+
# Table_Select.build_joined_query strategy!
|
|
169
|
+
concrete_base_joins.each_pair { |table, foreign_base_tables|
|
|
170
|
+
# Ignore base tables - their fields are added automatically
|
|
171
|
+
# via __attributes__.num_fields below.
|
|
172
|
+
base_model = concrete_base_klasses[table].first
|
|
173
|
+
break if base_model == self # Offset ends with own fields
|
|
174
|
+
inject_index += base_model.__attributes__.num_fields
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# Amount of polymorphic fields defined in this model
|
|
178
|
+
polymorphic_num_fields = @__attributes__.num_fields()
|
|
179
|
+
# Field offset should point to index where concrete
|
|
180
|
+
# fields begin.
|
|
181
|
+
field_offset = 0
|
|
182
|
+
@__associations__.concrete_models.each { |cm|
|
|
183
|
+
break if cm == concrete_model
|
|
184
|
+
concrete_model_index += 1
|
|
185
|
+
field_offset += (cm.__attributes__.num_fields - polymorphic_num_fields)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
field_offset_end = (field_offset + concrete_model.__attributes__.num_fields)
|
|
189
|
+
basic_values = values[0...polymorphic_num_fields]
|
|
190
|
+
concrete_values = values[(polymorphic_num_fields + field_offset)...field_offset_end]
|
|
191
|
+
|
|
192
|
+
# Basic values from polymorphic base model have to be injected
|
|
193
|
+
# into concrete values at inject_index.
|
|
194
|
+
instance_values = concrete_values[0...inject_index]
|
|
195
|
+
instance_values += basic_values # Inject happens here
|
|
196
|
+
instance_values += concrete_values[inject_index..-1]
|
|
197
|
+
|
|
198
|
+
concrete_model.new(instance_values, joined_models)
|
|
199
|
+
end # }}}
|
|
200
|
+
|
|
201
|
+
def self.disable_output_filters
|
|
202
|
+
@output_filters_disabled = true
|
|
203
|
+
end
|
|
204
|
+
def self.enable_output_filters
|
|
205
|
+
@output_filters_disabled = false
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def self.output_filters_disabled?
|
|
209
|
+
@output_filters_disabled || false
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def attribute_values_by_table
|
|
213
|
+
return @attribute_values if @attribute_values
|
|
214
|
+
|
|
215
|
+
# Note that attributes might have been shadowed
|
|
216
|
+
# in @attribute_values flat, as first attribute
|
|
217
|
+
# takes precedence in casse two tables with common
|
|
218
|
+
# field name have been joined.
|
|
219
|
+
values = @attribute_values_flat
|
|
220
|
+
|
|
221
|
+
@attribute_values = Hash.new
|
|
222
|
+
field_index = 0
|
|
223
|
+
|
|
224
|
+
models = [ self.class ]
|
|
225
|
+
models |= @joined_models
|
|
226
|
+
models.each { |model|
|
|
227
|
+
tables = model.all_table_names
|
|
228
|
+
fields = model.get_fields
|
|
229
|
+
tables.each { |table|
|
|
230
|
+
map = {}
|
|
231
|
+
fields[table].each { |field_name|
|
|
232
|
+
map[field_name] = values[field_index]
|
|
233
|
+
field_index += 1
|
|
234
|
+
}
|
|
235
|
+
@attribute_values[table] = map
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
# Applying filter to *all* attribute values, including derived attributes.
|
|
240
|
+
# This way, an output filter can be added in a derived type that does not
|
|
241
|
+
# exist in a base type.
|
|
242
|
+
output_filters = self.class.__filters__.output_filters
|
|
243
|
+
|
|
244
|
+
@attribute_values.values.map { |v|
|
|
245
|
+
v.each_pair { |attribute, value|
|
|
246
|
+
filter = output_filters[attribute]
|
|
247
|
+
value = filter.call(value) if filter
|
|
248
|
+
value
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
@attribute_values_flat = {}
|
|
253
|
+
@attribute_values.values.each { |map| @attribute_values_flat.update map }
|
|
254
|
+
|
|
255
|
+
return @attribute_values
|
|
256
|
+
end # }}}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# Simulates inheritance: Delegate missing methods to parent Table_Accessor.
|
|
261
|
+
def self.method_missing(meth)
|
|
262
|
+
# {{{
|
|
263
|
+
if @is_a_klasses then
|
|
264
|
+
@is_a_klasses.each_pair { |foreign_key, k|
|
|
265
|
+
return (k.__send__(meth.to_s)) if k.respond_to? meth
|
|
266
|
+
}
|
|
267
|
+
end
|
|
268
|
+
raise ::Exception.new('Undefined method '<< meth.to_s << ' for ' << self.to_s)
|
|
269
|
+
end # }}}
|
|
270
|
+
|
|
271
|
+
# Inspect method
|
|
272
|
+
def self.inspect
|
|
273
|
+
# {{{
|
|
274
|
+
'Lore::Table_Accessor: ' << self.to_s
|
|
275
|
+
end # }}}
|
|
276
|
+
|
|
277
|
+
# Recursively gets primary keys from parent, if own
|
|
278
|
+
# primary keys don't have been set, yet:
|
|
279
|
+
# Returns all derived primary keys WITHOUT OWN PKEYS.
|
|
280
|
+
def self.get_primary_keys
|
|
281
|
+
# {{{
|
|
282
|
+
@__attributes__.primary_keys
|
|
283
|
+
end # }}}
|
|
284
|
+
|
|
285
|
+
# Return primary key names of own table only,
|
|
286
|
+
# i.e. skipping inherited ones.
|
|
287
|
+
def self.get_own_primary_keys
|
|
288
|
+
# {{{
|
|
289
|
+
if !@own_primary_keys then
|
|
290
|
+
@own_primary_keys = @__attributes__.primary_keys[@table_name].uniq
|
|
291
|
+
end
|
|
292
|
+
@own_primary_keys
|
|
293
|
+
end # }}}
|
|
294
|
+
|
|
295
|
+
def self.key_array()
|
|
296
|
+
# {{{
|
|
297
|
+
# TODO: Use __attributes__ here
|
|
298
|
+
keys = Array.new
|
|
299
|
+
get_primary_keys.each_pair { |table, attribs|
|
|
300
|
+
attribs.each { |attrib|
|
|
301
|
+
keys.push attrib
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return keys
|
|
305
|
+
end # }}}
|
|
306
|
+
|
|
307
|
+
# Recursively gets sequences from parent, if own
|
|
308
|
+
# sequences don't have been set, yet:
|
|
309
|
+
def self.get_sequences
|
|
310
|
+
# {{{
|
|
311
|
+
|
|
312
|
+
if @sequences.nil? then
|
|
313
|
+
if @is_a_klasses.nil? then
|
|
314
|
+
return Hash.new
|
|
315
|
+
else
|
|
316
|
+
seq_map = Hash.new
|
|
317
|
+
@is_a_klasses.each_pair { |foreign_key,k|
|
|
318
|
+
seq_map[k.table_name] = k.get_sequences
|
|
319
|
+
}
|
|
320
|
+
return seq_map
|
|
321
|
+
end
|
|
322
|
+
else
|
|
323
|
+
return @sequences
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
end # }}}
|
|
327
|
+
|
|
328
|
+
def self.set_sequences(arg) # :nodoc:
|
|
329
|
+
# {{{
|
|
330
|
+
@sequences = arg
|
|
331
|
+
end # }}}
|
|
332
|
+
|
|
333
|
+
# Returns base table of this model as String.
|
|
334
|
+
def self.table_name
|
|
335
|
+
@table_name
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Returns all (i.e. including joined) tables as
|
|
339
|
+
# Array of Strings, ordered by join order.
|
|
340
|
+
def self.all_table_names
|
|
341
|
+
# {{{
|
|
342
|
+
return @table_names if @table_names
|
|
343
|
+
@table_names = [@table_name]
|
|
344
|
+
@table_names += @__associations__.joins.keys_flat
|
|
345
|
+
return @table_names
|
|
346
|
+
end # }}}
|
|
347
|
+
|
|
348
|
+
# Returns all attribute fields as Hash of Array of Strings,
|
|
349
|
+
# in the same order as defined in the table, mapped by
|
|
350
|
+
# table names.
|
|
351
|
+
# Example: (Article < Content)
|
|
352
|
+
#
|
|
353
|
+
# Article.get_fields
|
|
354
|
+
# -->
|
|
355
|
+
# {
|
|
356
|
+
# 'public.content' => [ :content_id, :title, :date ],
|
|
357
|
+
# 'public.article' => [ :author, :lead_in ]
|
|
358
|
+
# }
|
|
359
|
+
#
|
|
360
|
+
# Also see get_fields_flat.
|
|
361
|
+
#
|
|
362
|
+
def self.get_fields
|
|
363
|
+
@fields ||= @__attributes__.fields
|
|
364
|
+
@fields
|
|
365
|
+
end
|
|
366
|
+
# Returns all attribute fields as Hash of Array of Strings,
|
|
367
|
+
# in the same order as defined in the table, mapped by
|
|
368
|
+
# table names.
|
|
369
|
+
# Example: (Article < Content)
|
|
370
|
+
#
|
|
371
|
+
# Article.get_fields_flat
|
|
372
|
+
# -->
|
|
373
|
+
# [ :content_id, :title, :date, :author, :lead_in
|
|
374
|
+
#
|
|
375
|
+
# Also see example in documentation to get_fields.
|
|
376
|
+
#
|
|
377
|
+
def self.get_fields_flat
|
|
378
|
+
@fields_flat ||= @__attributes__.fields_flat
|
|
379
|
+
@fields_flat
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
protected
|
|
383
|
+
|
|
384
|
+
# If this model is not to be located in a projects default context,
|
|
385
|
+
# you can tell Cuba which context to use via
|
|
386
|
+
#
|
|
387
|
+
# context :other_context_name
|
|
388
|
+
#
|
|
389
|
+
def self.context(context_name)
|
|
390
|
+
# {{{
|
|
391
|
+
@context = context_name
|
|
392
|
+
end
|
|
393
|
+
def self.get_context
|
|
394
|
+
@context
|
|
395
|
+
end
|
|
396
|
+
# }}}
|
|
397
|
+
|
|
398
|
+
# Define the base table of this model.
|
|
399
|
+
# Usage:
|
|
400
|
+
#
|
|
401
|
+
# class Content < Lore::Model
|
|
402
|
+
# table :content, :public $ table name is 'public.content'
|
|
403
|
+
# ...
|
|
404
|
+
# end
|
|
405
|
+
#
|
|
406
|
+
def self.table(model_table, model_schema=nil)
|
|
407
|
+
# {{{
|
|
408
|
+
model_table = "#{model_schema}.#{model_table}" if model_schema
|
|
409
|
+
@table_name = model_table
|
|
410
|
+
init_model(); # Table name is all we need to bootstrap a model
|
|
411
|
+
end # }}}
|
|
412
|
+
|
|
413
|
+
# Usage:
|
|
414
|
+
#
|
|
415
|
+
# primary_key :some_field, :some_field_sequence_name
|
|
416
|
+
# primary_key :some_other_field, :some_other_field_sequence_name
|
|
417
|
+
#
|
|
418
|
+
def self.primary_key(*prim_key)
|
|
419
|
+
# {{{
|
|
420
|
+
@__associations__.add_primary_key(prim_key.at(0), prim_key.at(1))
|
|
421
|
+
@__attributes__.add_primary_key(prim_key.at(0), prim_key.at(1))
|
|
422
|
+
end # }}}
|
|
423
|
+
|
|
424
|
+
# Define this model as derived from another model class,
|
|
425
|
+
# which realized model inheritance.
|
|
426
|
+
# Note that ruby only allows only single inheritance itself,
|
|
427
|
+
# but is_a may be used for multiple inheritance, too.
|
|
428
|
+
# Usage:
|
|
429
|
+
#
|
|
430
|
+
# class Article < Content
|
|
431
|
+
# table :article, :public
|
|
432
|
+
# is_a Content, :content_id
|
|
433
|
+
# is_a Asset, :asset_id
|
|
434
|
+
# end
|
|
435
|
+
#
|
|
436
|
+
# Effects:
|
|
437
|
+
# Creating an Article record will also create referenced
|
|
438
|
+
# Content and Assed records.
|
|
439
|
+
# Selecting an Article entity will automatically join
|
|
440
|
+
# Content and Asset records.
|
|
441
|
+
# Filters and hooks from Content and Asset are inherited.
|
|
442
|
+
#
|
|
443
|
+
# Article.is_a?(Content) --> true
|
|
444
|
+
# Article.is_a?(Asset) --> true(!)
|
|
445
|
+
#
|
|
446
|
+
def self.is_a(*args)
|
|
447
|
+
# {{{
|
|
448
|
+
parent = args.at(0)
|
|
449
|
+
|
|
450
|
+
@__filters__.inherit(parent)
|
|
451
|
+
@__attributes__.add_base_model(parent)
|
|
452
|
+
@__associations__.add_base_model(parent, args[1..-1])
|
|
453
|
+
|
|
454
|
+
define_entity_access_methods(parent, args[1..-1])
|
|
455
|
+
end # }}}
|
|
456
|
+
|
|
457
|
+
# Usage in derived classes:
|
|
458
|
+
# aggregates Other::Module::Other_Klass
|
|
459
|
+
# aggregates Another::Module::Another_Klass
|
|
460
|
+
#
|
|
461
|
+
# Effects:
|
|
462
|
+
# Performs eager join on given model on every select on this model.
|
|
463
|
+
# Unlike is_a, If foo.aggregates bar then creating/deleting a foo will not
|
|
464
|
+
# create/delete bar instance, but loading a foo will aggregate bar
|
|
465
|
+
# automatically, like is_a.
|
|
466
|
+
def self.aggregates(*args)
|
|
467
|
+
# {{{
|
|
468
|
+
parent = args.at(0)
|
|
469
|
+
|
|
470
|
+
@__filters__.inherit(parent)
|
|
471
|
+
@__attributes__.add_aggregate_model(parent)
|
|
472
|
+
@__associations__.add_aggregate_model(parent, args[1..-1])
|
|
473
|
+
|
|
474
|
+
define_entity_access_methods(parent, args[1..-1])
|
|
475
|
+
end # }}}
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
class << self
|
|
479
|
+
alias org_is_a? is_a?
|
|
480
|
+
def is_a?(model)
|
|
481
|
+
org_is_a?(model) || @__associations__ && @__associations__.has_joined_model?(model)
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# Usage:
|
|
486
|
+
#
|
|
487
|
+
# has_a Other_Model, :foreign_key_1 <, foreign_key_2, ... >
|
|
488
|
+
#
|
|
489
|
+
# note that foreign_keys are fields in _this_ table,
|
|
490
|
+
# not the foreign one.
|
|
491
|
+
#
|
|
492
|
+
def self.has_a(*args)
|
|
493
|
+
# {{{
|
|
494
|
+
@__associations__.add_has_a(args.at(0), args[1..-1])
|
|
495
|
+
define_entity_access_methods(args.at(0), args[1..-1])
|
|
496
|
+
end # }}}
|
|
497
|
+
|
|
498
|
+
# Usage:
|
|
499
|
+
#
|
|
500
|
+
# has_n Other_Model, :foreign_key_1 <, foreign_key_2, ... >
|
|
501
|
+
#
|
|
502
|
+
# note that foreign_keys are fields in _this_ table,
|
|
503
|
+
# not the foreign one.
|
|
504
|
+
#
|
|
505
|
+
def self.has_n(other, *args)
|
|
506
|
+
# {{{
|
|
507
|
+
@__associations__.add_has_n(other, *args)
|
|
508
|
+
define_entities_access_methods(args.at(0), args[1..-1])
|
|
509
|
+
end # }}}
|
|
510
|
+
|
|
511
|
+
# Usage:
|
|
512
|
+
#
|
|
513
|
+
# maps Model_A => [ :foreign_keys, ... ], Model_B => [ foreign_keys, ... ]
|
|
514
|
+
#
|
|
515
|
+
# note that foreign_keys are fields in _this_ table,
|
|
516
|
+
# not the foreign one.
|
|
517
|
+
#
|
|
518
|
+
def self.maps(*accessors)
|
|
519
|
+
# {{{
|
|
520
|
+
@__associations__.add_mapping(*accessors)
|
|
521
|
+
end # }}}
|
|
522
|
+
|
|
523
|
+
def self.validates(attrib, constraints)
|
|
524
|
+
# {{{
|
|
525
|
+
@__attributes__.add_constraints(attrib, constraints)
|
|
526
|
+
end # }}}
|
|
527
|
+
|
|
528
|
+
def self.add_input_filter(attrib, &block)
|
|
529
|
+
@__filters__.add_input_filter(attrib, &block)
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def self.add_output_filter(attrib, &block)
|
|
533
|
+
@__filters__.add_output_filter(attrib, &block)
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Demands a value to be set for create and update procedures.
|
|
537
|
+
def self.expects(attrib_name, klass=nil)
|
|
538
|
+
# {{{
|
|
539
|
+
@__attributes__.set_required(attrib_name)
|
|
540
|
+
end # }}}
|
|
541
|
+
|
|
542
|
+
def self.explicit(*args)
|
|
543
|
+
Aurita.log { "Model.explicit is deprecated (called for #{self.to_s}" }
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
def self.hide_attribute(*args)
|
|
547
|
+
Aurita.log { "Model.hidden_attribute is deprecated (called for #{self.to_s}.
|
|
548
|
+
Use Model.hidden instead" }
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# Define an attribute as hidden.
|
|
552
|
+
# Especially needed for form generation (hidden fields)
|
|
553
|
+
def self.hides(attrib_name)
|
|
554
|
+
# {{{
|
|
555
|
+
@__attributes__.add_hidden(attrib_name)
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def self.get_hidden()
|
|
559
|
+
return @hidden_attributes if @hidden_attributes
|
|
560
|
+
Hash.new
|
|
561
|
+
end # }}}
|
|
562
|
+
|
|
563
|
+
def self.belongs_to(*args)
|
|
564
|
+
# {{{
|
|
565
|
+
@__associations__.add_belongs_to(args.at(0), args[1..-1])
|
|
566
|
+
end # }}}
|
|
567
|
+
|
|
568
|
+
# Define attribute to use as label for instances of a Table_Accessor,
|
|
569
|
+
# e.g. for select boxes.
|
|
570
|
+
def self.use_label(*attribs)
|
|
571
|
+
# {{{
|
|
572
|
+
@labels = attribs.map { |e|
|
|
573
|
+
|
|
574
|
+
e = e.to_s # also removes Array wraps like [:attrib_name]
|
|
575
|
+
|
|
576
|
+
if((e.kind_of? Clause) or (e.include?('.'))) then
|
|
577
|
+
e = e.to_s
|
|
578
|
+
else
|
|
579
|
+
e = @table_name + '.' << e.to_s
|
|
580
|
+
end
|
|
581
|
+
}
|
|
582
|
+
@label = attribs[0]
|
|
583
|
+
end
|
|
584
|
+
# Returns full name of attribute set to use as label e.g. for
|
|
585
|
+
# select boxes.
|
|
586
|
+
def self.get_label
|
|
587
|
+
@labels[0]
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def self.get_labels
|
|
591
|
+
@labels
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
# }}}
|
|
595
|
+
|
|
596
|
+
public
|
|
597
|
+
|
|
598
|
+
# Returns full attribute name of given attribute
|
|
599
|
+
def self.[](attribute_name)
|
|
600
|
+
# {{{
|
|
601
|
+
return "#{@table_name}.#{attribute_name}"
|
|
602
|
+
end # }}}
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def self.update(&block)
|
|
606
|
+
# {{{
|
|
607
|
+
query_string = @update_strategy.block_update(&block)
|
|
608
|
+
end # def }}}
|
|
609
|
+
|
|
610
|
+
def self.select(clause=nil, &block)
|
|
611
|
+
# {{{
|
|
612
|
+
if(!clause.nil? && !clause.to_s.include?('*,')) then
|
|
613
|
+
query_string = @select_strategy.select_query(clause.to_s, &block)
|
|
614
|
+
return Clause.new(query_string[:query])
|
|
615
|
+
end
|
|
616
|
+
return Select_Query.new(self, clause.to_s, &block)
|
|
617
|
+
end # }}}
|
|
618
|
+
|
|
619
|
+
def self.select_query(clause=nil, &block)
|
|
620
|
+
query_string = @select_strategy.select_query(clause.to_s, &block)
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
def self.polymorphic_select(clause=nil, &block)
|
|
624
|
+
# {{{
|
|
625
|
+
if(!clause.nil? && !clause.to_s.include?('*,')) then
|
|
626
|
+
query_string = @select_strategy.select_query(clause.to_s, nil, true, &block)
|
|
627
|
+
return Clause.new(query_string[:query])
|
|
628
|
+
end
|
|
629
|
+
return Select_Query.new(self, clause.to_s, true, &block)
|
|
630
|
+
end # }}}
|
|
631
|
+
|
|
632
|
+
def self.polymorphic_select_query(clause=nil, &block)
|
|
633
|
+
query_string = @select_strategy.select_query(clause.to_s, nil, true, &block)
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
# Same as select, but returns scalar value.
|
|
637
|
+
def self.select_value(what, &block)
|
|
638
|
+
# {{{
|
|
639
|
+
db_result = @select_strategy.select(what, &block)
|
|
640
|
+
row = db_result.get_row
|
|
641
|
+
return row.first if row.first
|
|
642
|
+
return {}
|
|
643
|
+
end # }}}
|
|
644
|
+
|
|
645
|
+
def self.select_values(what, &block)
|
|
646
|
+
# {{{
|
|
647
|
+
@select_strategy.select(what, &block).get_rows
|
|
648
|
+
end # }}}
|
|
649
|
+
|
|
650
|
+
# Same as select, but returns scalar value.
|
|
651
|
+
def self.polymorphic_select_value(what, &block)
|
|
652
|
+
# {{{
|
|
653
|
+
db_result = @select_strategy.select(what, true, &block)
|
|
654
|
+
row = db_result.get_row
|
|
655
|
+
return row.first if row.first
|
|
656
|
+
return {}
|
|
657
|
+
end # }}}
|
|
658
|
+
|
|
659
|
+
def self.polymorphic_select_values(what, &block)
|
|
660
|
+
# {{{
|
|
661
|
+
@select_strategy.select(what, true, &block).get_rows
|
|
662
|
+
end # }}}
|
|
663
|
+
|
|
664
|
+
# Wrap explicit select. Example:
|
|
665
|
+
# SomeModule::SomeAccessor.explicit_insert({
|
|
666
|
+
# table_name_A =>
|
|
667
|
+
# {'some_field'=>'2',
|
|
668
|
+
# 'other_field'=>'3'},
|
|
669
|
+
# table_name_A =>
|
|
670
|
+
# {'another_field'=>'5'}
|
|
671
|
+
# })
|
|
672
|
+
# Note that field in 'field'=>'value' is exactly the
|
|
673
|
+
# field name in the table (e.g. table_name_A) it holds.
|
|
674
|
+
def self.explicit_insert(keys)
|
|
675
|
+
# {{{
|
|
676
|
+
@insert_strategy.perform_insert(keys)
|
|
677
|
+
end # }}}
|
|
678
|
+
|
|
679
|
+
# Wrap default select. Example:
|
|
680
|
+
# SomeModule::SomeAccessor.insert({
|
|
681
|
+
# 'some_field'=>'2',
|
|
682
|
+
# 'other_field'=>'3',
|
|
683
|
+
# 'another_field'=>'5'
|
|
684
|
+
# })
|
|
685
|
+
# Note that field in 'field'=>'value' is exactly the
|
|
686
|
+
# field name in the table it holds.
|
|
687
|
+
# Table_Accessor.insert basically resolves an explicit
|
|
688
|
+
# hash and passes it to Table_Accessor.explicit_insert.
|
|
689
|
+
def self.insert(keys)
|
|
690
|
+
# {{{
|
|
691
|
+
# Sequence values only are known after insert operation,
|
|
692
|
+
# so we have to retreive the complete key_hash back from
|
|
693
|
+
# Table_Inserter.perform_insert:
|
|
694
|
+
key_hash = @insert_strategy.perform_insert(keys)
|
|
695
|
+
# key_hash has been extended by sequence_values now, so
|
|
696
|
+
# we return it:
|
|
697
|
+
key_hash
|
|
698
|
+
end # }}}
|
|
699
|
+
|
|
700
|
+
def self.distribute_attrib_values(attrib_values)
|
|
701
|
+
# {{{
|
|
702
|
+
values = {}
|
|
703
|
+
# Predefine
|
|
704
|
+
attrib_name_array = []
|
|
705
|
+
# distribute attrib names to tables:
|
|
706
|
+
@__attributes__.fields().each_pair { |table, attribs|
|
|
707
|
+
table_values = {}
|
|
708
|
+
attrib_name_array = []
|
|
709
|
+
|
|
710
|
+
attrib_values.each_pair { |attrib_name, attrib_value|
|
|
711
|
+
attrib_name_array = attrib_name.split('.') unless attrib_name.instance_of?(Symbol)
|
|
712
|
+
attrib_name_array ||= []
|
|
713
|
+
attrib_short_name = false
|
|
714
|
+
if attrib_name_array.at(2)
|
|
715
|
+
attrib_short_name = attrib_name_array.at(2)
|
|
716
|
+
else
|
|
717
|
+
attrib_name = attrib_name.to_sym
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
if attribs.include? attrib_name then
|
|
721
|
+
table_values[attrib_name] = attrib_value
|
|
722
|
+
elsif attrib_short_name &&
|
|
723
|
+
attribs.include?(attrib_short_name) &&
|
|
724
|
+
table == "#{attrib_name_array.at(0)}.#{attrib_name_array.at(1)}" then
|
|
725
|
+
table_values[attrib_name_array.at(2)] = attrib_value
|
|
726
|
+
end
|
|
727
|
+
}
|
|
728
|
+
values[table] = table_values
|
|
729
|
+
}
|
|
730
|
+
values
|
|
731
|
+
end # }}}
|
|
732
|
+
|
|
733
|
+
# Returns a new Table_Accessor instance by inserting given attribute
|
|
734
|
+
# values into db and returning an instance for further operations.
|
|
735
|
+
def self.create(attrib_values={})
|
|
736
|
+
# {{{
|
|
737
|
+
attrib_values[:concrete_model] = self.to_s
|
|
738
|
+
before_create(attrib_values)
|
|
739
|
+
|
|
740
|
+
input_filters = @__filters__.input_filters
|
|
741
|
+
attrib_key = ''
|
|
742
|
+
attrib_name = ''
|
|
743
|
+
|
|
744
|
+
attrib_values.each_pair { |attrib_name, attrib_value|
|
|
745
|
+
if attrib_name.instance_of? Symbol then
|
|
746
|
+
attrib_key = attrib_name
|
|
747
|
+
else
|
|
748
|
+
attrib_key = attrib_name.split('.')[-1].to_sym
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
if (input_filters && input_filters[attrib_key]) then
|
|
752
|
+
attrib_values[attrib_name] = input_filters[attrib_key].call(attrib_value)
|
|
753
|
+
end
|
|
754
|
+
}
|
|
755
|
+
after_filters(attrib_values)
|
|
756
|
+
|
|
757
|
+
values = distribute_attrib_values(attrib_values)
|
|
758
|
+
|
|
759
|
+
before_validation(values)
|
|
760
|
+
if @__associations__.polymorphics then
|
|
761
|
+
Lore.logger.debug { "Polymorphic create on #{self.to_s}" }
|
|
762
|
+
@__associations__.polymorphics.each_pair { |table, model_field|
|
|
763
|
+
values[table][model_field] = self.to_s
|
|
764
|
+
}
|
|
765
|
+
end
|
|
766
|
+
Lore::Validation::Parameter_Validator.validate(self, values)
|
|
767
|
+
|
|
768
|
+
before_insert(attrib_values)
|
|
769
|
+
|
|
770
|
+
# retreive all final attrib values after insert: (this way, also
|
|
771
|
+
# sequence values are resolved):
|
|
772
|
+
|
|
773
|
+
attrib_values = @insert_strategy.perform_insert(values)
|
|
774
|
+
|
|
775
|
+
# This would be a double check, as self.load already filters
|
|
776
|
+
# non-primary key attributes
|
|
777
|
+
select_keys = Hash.new
|
|
778
|
+
@__associations__.primary_keys[table_name].each { |key|
|
|
779
|
+
select_keys["#{table_name}.#{key}"] = attrib_values[table_name][key]
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
obj = self.load(select_keys)
|
|
783
|
+
raise ::Exception.new("Could not load created instance of #{self.to_s}: #{select_keys.inspect}") unless obj
|
|
784
|
+
after_create(obj)
|
|
785
|
+
|
|
786
|
+
return obj
|
|
787
|
+
|
|
788
|
+
end # }}}
|
|
789
|
+
|
|
790
|
+
# Return new Table_Accessor instance by loading an existing entry from
|
|
791
|
+
# table if present, or false if no entry has been found.
|
|
792
|
+
# Accepts any combination of :primary_key => 'value'
|
|
793
|
+
# Also allows inherited primary keys.
|
|
794
|
+
def self.load(keys)
|
|
795
|
+
# {{{
|
|
796
|
+
before_load(keys)
|
|
797
|
+
|
|
798
|
+
select_keys = {}
|
|
799
|
+
value = false
|
|
800
|
+
@__associations__.primary_keys.each_pair { |table, pkeys|
|
|
801
|
+
pkeys.each { |attrib_name|
|
|
802
|
+
full_attrib_name = "#{table}.#{attrib_name}"
|
|
803
|
+
value = keys[full_attrib_name] # The more explicit, the better.
|
|
804
|
+
value ||= keys[attrib_name.to_sym] # Symbols are supposed to be most frequently used
|
|
805
|
+
value ||= keys[attrib_name.to_s]
|
|
806
|
+
select_keys[full_attrib_name] = value unless value.nil?
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
return false if select_keys.empty?
|
|
811
|
+
|
|
812
|
+
# We have to perform a select here instead of returning
|
|
813
|
+
# the instance with given attribute values, as this is the
|
|
814
|
+
# only way to retreive attribute values set in the DB via
|
|
815
|
+
# default values, triggers, etc.
|
|
816
|
+
c = Clause.new
|
|
817
|
+
select_keys.each_pair { |k,v|
|
|
818
|
+
c & (Clause.new(k.to_s.dup) == v.to_s)
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
instance = self.select { |inst|
|
|
822
|
+
inst.where(c)
|
|
823
|
+
inst.limit(1)
|
|
824
|
+
}.first
|
|
825
|
+
|
|
826
|
+
return false unless instance
|
|
827
|
+
return instance
|
|
828
|
+
|
|
829
|
+
end # }}}
|
|
830
|
+
|
|
831
|
+
# Load an instance by only providing primary key values.
|
|
832
|
+
# This is useful for polymorphic treatment of models, as
|
|
833
|
+
# you don't have to know about primary key names.
|
|
834
|
+
#
|
|
835
|
+
# Example:
|
|
836
|
+
#
|
|
837
|
+
# The_Model.get(123)
|
|
838
|
+
#
|
|
839
|
+
# Resolves to
|
|
840
|
+
#
|
|
841
|
+
# The_Model.load(:the_model_id => 123)
|
|
842
|
+
#
|
|
843
|
+
# If there is more than one primary key attribute,
|
|
844
|
+
# key values have to be provided in the same order they
|
|
845
|
+
# are specified in the database.
|
|
846
|
+
#
|
|
847
|
+
def self.get(*key_values)
|
|
848
|
+
# {{{
|
|
849
|
+
pkeys = {}
|
|
850
|
+
get_primary_keys[table_name].uniq.each_with_index { |pkey, idx|
|
|
851
|
+
pkeys[pkey] = key_values.at(idx)
|
|
852
|
+
}
|
|
853
|
+
load(pkeys)
|
|
854
|
+
end # }}}
|
|
855
|
+
|
|
856
|
+
# Delete this object and use Table_Deleter to delete its entity tuple
|
|
857
|
+
# from database.
|
|
858
|
+
def self.delete(value_keys=nil, &block)
|
|
859
|
+
# {{{
|
|
860
|
+
if value_keys then
|
|
861
|
+
before_delete(value_keys)
|
|
862
|
+
@delete_strategy.perform_delete(value_keys)
|
|
863
|
+
after_delete(value_keys)
|
|
864
|
+
else
|
|
865
|
+
@delete_strategy.block_delete(&block)
|
|
866
|
+
end
|
|
867
|
+
end # }}}
|
|
868
|
+
|
|
869
|
+
private
|
|
870
|
+
|
|
871
|
+
# Send a hollow query to db, thus only field names are returned.
|
|
872
|
+
# This works (and has to work) on empty tables, too.
|
|
873
|
+
#
|
|
874
|
+
# For every attribute, a class method with the attribute's name is defined
|
|
875
|
+
# in order to retreive the absolute field name (schema.table.field) by calling
|
|
876
|
+
# Table_Accessor.field
|
|
877
|
+
def self.init_model()
|
|
878
|
+
# {{{
|
|
879
|
+
Lore::Context.enter(@context) unless @context.nil?
|
|
880
|
+
begin
|
|
881
|
+
fields_result = Lore::Connection.perform("SELECT * FROM #{@table_name} WHERE false")
|
|
882
|
+
ensure
|
|
883
|
+
Lore::Context.leave unless @context.nil?
|
|
884
|
+
end
|
|
885
|
+
@__attributes__ = Attribute_Settings.new(self,
|
|
886
|
+
fields_result.field_names(),
|
|
887
|
+
fields_result.field_types())
|
|
888
|
+
|
|
889
|
+
@__associations__ = Associations.new(self)
|
|
890
|
+
|
|
891
|
+
@__filters__ = Filters.new(self)
|
|
892
|
+
@__attributes__.types.each_pair { |table, attributes|
|
|
893
|
+
attributes.each_pair { |field,type|
|
|
894
|
+
input_filter = Lore::Type_Filters.in[type]
|
|
895
|
+
output_filter = Lore::Type_Filters.out[type]
|
|
896
|
+
@__filters__.add_input_filter(field, &input_filter) if input_filter
|
|
897
|
+
@__filters__.add_output_filter(field, &output_filter) if output_filter
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
@select_strategy = Table_Select.new(self)
|
|
902
|
+
@insert_strategy = Table_Insert.new(self)
|
|
903
|
+
@delete_strategy = Table_Delete.new(self)
|
|
904
|
+
@update_strategy = Table_Update.new(self)
|
|
905
|
+
|
|
906
|
+
define_attribute_clause_methods()
|
|
907
|
+
end # }}}
|
|
908
|
+
|
|
909
|
+
def self.define_attribute_clause_methods
|
|
910
|
+
# {{{
|
|
911
|
+
if !@context.nil? then
|
|
912
|
+
Lore::Context.enter(@context)
|
|
913
|
+
context_switched = true
|
|
914
|
+
end
|
|
915
|
+
if @__attributes__[@table_name] then
|
|
916
|
+
@__attributes__[@table_name].each { |attribute|
|
|
917
|
+
# Only define methods on own attributes or on
|
|
918
|
+
# attributes only a parent Table_Accessor provides:
|
|
919
|
+
attribute_type = @__attributes__.types[@table_name][attribute]
|
|
920
|
+
method =
|
|
921
|
+
"def self.#{attribute}()
|
|
922
|
+
Clause.new('#{@table_name}.#{attribute}', '', '', { :add_types => [ #{attribute_type}], :add_name => '' })
|
|
923
|
+
end"
|
|
924
|
+
class_eval(method)
|
|
925
|
+
|
|
926
|
+
"def #{attribute}()
|
|
927
|
+
attr(:#{attribute})
|
|
928
|
+
end"
|
|
929
|
+
class_eval(method)
|
|
930
|
+
|
|
931
|
+
method =
|
|
932
|
+
"def set_#{attribute}(value)
|
|
933
|
+
set_attribute_value(:#{attribute.to_s}, value)
|
|
934
|
+
end"
|
|
935
|
+
class_eval(method)
|
|
936
|
+
|
|
937
|
+
method =
|
|
938
|
+
"def #{attribute}=(value)
|
|
939
|
+
set_attribute_value(:#{attribute.to_s}, value)
|
|
940
|
+
end"
|
|
941
|
+
class_eval(method)
|
|
942
|
+
}
|
|
943
|
+
end
|
|
944
|
+
Lore::Context.leave if context_switched
|
|
945
|
+
|
|
946
|
+
end # }}}
|
|
947
|
+
|
|
948
|
+
# Meta-programs class instance methods for accessing types
|
|
949
|
+
# associated via has_a.
|
|
950
|
+
def self.define_entity_access_methods(accessor, foreign_keys, type_name=nil)
|
|
951
|
+
# {{{
|
|
952
|
+
type_name = accessor.to_s.split('::').at(-1).downcase unless type_name
|
|
953
|
+
|
|
954
|
+
define_method(type_name) {
|
|
955
|
+
|
|
956
|
+
has_a_keys = Hash.new
|
|
957
|
+
foreign_keys.each { |foreign_key|
|
|
958
|
+
# self.<foreign_key> will return corresponding value
|
|
959
|
+
# no matter if this klass or a super klass is holding it
|
|
960
|
+
has_a_keys[accessor[foreign_key]] = self.__send__ foreign_key
|
|
961
|
+
}
|
|
962
|
+
return accessor.load(has_a_keys)
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
define_method("set_#{type_name}") { |other|
|
|
966
|
+
foreign_keys.each { |foreign_key|
|
|
967
|
+
# other.<foreign_key> will return corresponding value
|
|
968
|
+
# no matter if this klass or a super klass is holding it
|
|
969
|
+
self[foreign_key] = other.pkey
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
define_method("#{type_name}=") { |other|
|
|
974
|
+
self.__send__("set_#{type_name}", other)
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
define_method("set_#{type_name}!") { |other|
|
|
978
|
+
self.__send__("set_#{type_name}", other)
|
|
979
|
+
self.__send__("commit")
|
|
980
|
+
}
|
|
981
|
+
end # }}}
|
|
982
|
+
|
|
983
|
+
# Meta-programs class instance methods for accessing types
|
|
984
|
+
# associated via has_n.
|
|
985
|
+
def self.define_entities_access_methods(accessor, values)
|
|
986
|
+
# {{{
|
|
987
|
+
type_name = accessor.to_s.split('::').at(-1).downcase unless type_name
|
|
988
|
+
|
|
989
|
+
define_method('add_'+type_name) { |values|
|
|
990
|
+
values.update(get_primary_key_values)
|
|
991
|
+
accessor.create(values)
|
|
992
|
+
}
|
|
993
|
+
define_method(type_name+'_list') {
|
|
994
|
+
foreign_key_values = Hash.new
|
|
995
|
+
|
|
996
|
+
accessor.get_foreign_keys[self.table_name].each { |key|
|
|
997
|
+
foreign_key_values[key] = get_attribute_value[key]
|
|
998
|
+
}
|
|
999
|
+
accessor.all_with(foreign_key_values)
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
define_method("remove_#{type_name}") { |*keys|
|
|
1003
|
+
# For usage:
|
|
1004
|
+
# Car.remove_wheel(wheel_obj)
|
|
1005
|
+
#
|
|
1006
|
+
if keys.length == 1 && keys.first.respond_to?(:pkey) then
|
|
1007
|
+
keys = [ keys.first.pkey ]
|
|
1008
|
+
end
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
end # }}}
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
end # class
|
|
1015
|
+
|
|
1016
|
+
end # module Lore
|