flexirecord 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ # Copyright (c) 2007 FlexiGuided GmbH, Berlin
2
+ #
3
+ # Author: Jan Behrens
4
+ #
5
+ # Website: http://www.flexiguided.de/publications.flexirecord.en.html
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ # of this software and associated documentation files (the "Software"), to deal
9
+ # in the Software without restriction, including without limitation the rights
10
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ # copies of the Software, and to permit persons to whom the Software is
12
+ # furnished to do so, subject to the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be included in
15
+ # all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ # SOFTWARE.
@@ -0,0 +1,33 @@
1
+ CREATE TABLE "person" (
2
+ "id" serial8 primary key,
3
+ "name" text not null );
4
+
5
+ CREATE TABLE "medium" (
6
+ "number" int8 primary key,
7
+ "lent_to_id" int8 references "person" ("id")
8
+ on delete restrict on update cascade );
9
+
10
+ CREATE TABLE "movie" (
11
+ "id" serial8 primary key,
12
+ "name" text not null );
13
+
14
+ CREATE TABLE "medium_entry" (
15
+ "medium_number" int8 not null references "medium" ("number")
16
+ on delete cascade on update cascade,
17
+ "position" int8 not null,
18
+ "movie_id" int8 not null
19
+ references "movie" ("id")
20
+ on delete restrict on update cascade,
21
+ PRIMARY KEY ("medium_number", "position") );
22
+
23
+ CREATE TABLE "rating" (
24
+ "person_id" int8 not null
25
+ references "person" ("id")
26
+ on delete cascade on update cascade,
27
+ "movie_id" int8 not null
28
+ references "movie" ("id")
29
+ on delete cascade on update cascade,
30
+ "rating" numeric,
31
+ "comment" text,
32
+ PRIMARY KEY ("person_id", "movie_id") );
33
+
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'flexirecord'
4
+
5
+ # Copyright (c) 2007 FlexiGuided GmbH, Berlin
6
+ #
7
+ # Author: Jan Behrens
8
+ #
9
+ # Website: http://www.flexiguided.de/publications.flexirecord.en.html
10
+ #
11
+ # -----
12
+ #
13
+ # Demonstration module for FlexiRecord.
14
+
15
+ module FlexiRecordDemo
16
+
17
+ # FlexiRecord::ConnectionPool used for all models in this module.
18
+ ConnectionPool = FlexiRecord::BaseRecord.connection_pool = FlexiRecord::ConnectionPool.new(:engine => :postgresql, :db => 'moviedemo')
19
+
20
+ # A person (demo class).
21
+ #
22
+ # CREATE TABLE "person" ("id" serial8 primary key, "name" text not null );
23
+ class Person < FlexiRecord::BaseRecord
24
+ self.table_name = 'person'
25
+ end
26
+
27
+ # A Medium (demo class).
28
+ #
29
+ # CREATE TABLE "medium" ("number" int8 primary key, "lent_to_id" int8 references "person" ("id") on delete restrict on update cascade );
30
+ class Medium < FlexiRecord::BaseRecord
31
+ self.table_name = 'medium'
32
+ add_many_to_one_reference Person, ['lent_to_id', 'id'], :lent_to, :borrowed_media
33
+ def self.after_select(records)
34
+ super
35
+ records.preload(:entries).preload(:movie)
36
+ records.preload(:lent_to)
37
+ records.preload(:movies)
38
+ end
39
+ def save
40
+ if self.number == :auto
41
+ self.class.transaction self, :read_committed do
42
+ self.class.db_execute("LOCK TABLE #{self.class.table} IN SHARE ROW EXCLUSIVE MODE")
43
+ last_medium = Medium.select1('ORDER BY "number" DESC LIMIT 1')
44
+ self.number = if last_medium
45
+ last_medium.number + 1
46
+ else
47
+ 1
48
+ end
49
+ return super
50
+ end
51
+ else
52
+ return super
53
+ end
54
+ end
55
+ def available?
56
+ lent_to.nil?
57
+ end
58
+ end
59
+
60
+ # A movie (demo class).
61
+ #
62
+ # CREATE TABLE "movie" ("id" serial8 primary key, "name" text not null );
63
+ class Movie < FlexiRecord::BaseRecord
64
+ self.table_name = 'movie'
65
+ end
66
+
67
+ # A medium entry (demo class).
68
+ #
69
+ # CREATE TABLE "medium_entry" ("medium_number" int8 not null references "medium" ("number") on delete cascade on update cascade, "position" int8 not null, "movie_id" int8 not null references "movie" ("id") on delete restrict on update cascade, PRIMARY KEY ("medium_number", "position") );
70
+ class MediumEntry < FlexiRecord::BaseRecord
71
+ include FlexiRecord::ListRecord
72
+ self.table_name = 'medium_entry'
73
+ add_many_to_one_reference(Medium, 'medium_', :medium, :entries).combine(
74
+ add_many_to_one_reference(Movie, 'movie_', :movie, :movie_entries),
75
+ :media, :movies
76
+ )
77
+ Medium.add_read_option :entries, :default, 'ORDER BY "position"'
78
+ Medium.add_read_option :movies, :default, 'ORDER BY "rel"."position"'
79
+ end
80
+
81
+ # A rating entry (demo class).
82
+ #
83
+ # CREATE TABLE "rating" ("person_id" int8 not null references "person" ("id") on delete cascade on update cascade, "movie_id" int8 not null references "movie" ("id") on delete cascade on update cascade, "rating" numeric, "comment" text, PRIMARY KEY ("person_id", "movie_id") );
84
+ class Rating < FlexiRecord::Relationship
85
+ self.table_name = 'rating'
86
+ add_many_to_one_reference(Person, 'person_', :person, :ratings).combine(
87
+ add_many_to_one_reference(Movie, ['movie_id', 'id'], :movie, :ratings),
88
+ :rated_by, :rated_movies
89
+ )
90
+ end
91
+
92
+
93
+ # A small demonstration program. In order to be run, a database named 'moviedemo' has to be installed and initialized with the 'flexirecord-demo.sql' file, which is shipped with the software package.
94
+ def demo
95
+ # Creating demo entries
96
+ Person.transaction do
97
+ Rating.db_execute( "DELETE FROM #{Rating.table}" )
98
+ MediumEntry.db_execute("DELETE FROM #{MediumEntry.table}")
99
+ Movie.db_execute( "DELETE FROM #{Movie.table}" )
100
+ Medium.db_execute( "DELETE FROM #{Medium.table}" )
101
+ Person.db_execute( "DELETE FROM #{Person.table}" )
102
+ end
103
+ anja = Person.new(:name => 'Anja' ).save
104
+ phillip = Person.new(:name => 'Phillip').save
105
+ wilson = Person.new(:name => 'Wilson' ).save
106
+ american_beauty = Movie.new(:name => 'American Beauty').save
107
+ naruto = Movie.new(:name => 'Naruto').save
108
+ koyaanisqatsi = Movie.new(:name => 'Koyaanisqatsi').save
109
+ medium_a = nil
110
+ Medium.transaction do
111
+ medium_a = Medium.new(:number => :auto).save
112
+ FlexiRecordDemo::MediumEntry.new(:medium => medium_a, :position => :last, :movie => naruto).save
113
+ end
114
+ medium_b = nil
115
+ Medium.transaction do
116
+ medium_b = Medium.new(:number => '42', :lent_to => phillip).save
117
+ MediumEntry.new(:medium => medium_b, :position => :last, :movie => koyaanisqatsi).save
118
+ MediumEntry.new(:medium => medium_b, :position => :last, :movie => american_beauty).save
119
+ end
120
+ Rating.new(:person => anja, :movie => naruto, :rating => Rational(7, 10)).save
121
+ Rating.new(:person => phillip, :movie => koyaanisqatsi, :rating => Rational(6,10)).save
122
+ Rating.new(:person => phillip, :movie => koyaanisqatsi, :rating => Rational(8,10)).save
123
+ Rating.new(:person => wilson, :movie => naruto, :comment => 'Rasengan!').save
124
+ # Some queries
125
+ person = Person.select1('WHERE "name" ILIKE $ ORDER BY "name" DESC LIMIT 1', 'P%')
126
+ puts "First person whose name is starting with 'P' is: #{person.name}."
127
+ puts "The following media are borrowed by him/her:"
128
+ person.borrowed_media.each do |medium|
129
+ puts "- ##{medium.number}"
130
+ medium.entries.each do |entry|
131
+ puts " - #{entry.movie.name}"
132
+ end
133
+ end
134
+ puts "He rated the following movies:"
135
+ person.rated_movies.each do |movie|
136
+ rating = movie.rel
137
+ puts "- #{movie.name}"
138
+ puts " - Rating: #{rating.rating ? rating.rating.to_s : 'none'}"
139
+ puts " - Comment: #{rating.comment || 'none'}"
140
+ end
141
+ nil
142
+ end
143
+ module_function :demo
144
+
145
+ end
@@ -0,0 +1,1425 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ #--
4
+ # Uncomment the following line to get debug output written to STDERR.
5
+ #$flexirecord_debug_output = STDERR
6
+
7
+ require 'postgres'
8
+ require 'monitor'
9
+ require 'thread_resource_pool'
10
+ require 'rational'
11
+
12
+
13
+ # Copyright (c) 2007 FlexiGuided GmbH, Berlin
14
+ #
15
+ # Author: Jan Behrens
16
+ #
17
+ # Website: http://www.flexiguided.de/publications.flexirecord.en.html
18
+ #
19
+ # -----
20
+ #
21
+ # FlexiRecord is a library for object oriented access to databases. Each table is represented by a class, each row of that table is represented by an object of that class. This library is especially aimed to properly support database transactions. By now only PostgreSQL (version 8.2 or higher!) is supported as a backend.
22
+ #
23
+ # Please note that this is an alpha release. This means the library is mostly untested yet. Use it at your own risk.
24
+ #
25
+ # To use FlexiRecord, you have to first create a new ConnectionPool with ConnectionPool.new. If all tables are stored on the same database, you can directly assign the ConnectionPool to the BaseRecord class, by calling:
26
+ #
27
+ # BaseRecord.connection_pool = ConnectionPool.new(...)
28
+ #
29
+ # You should also create sub-classes of BaseRecord, representing the tables of your database.
30
+ #
31
+ # There is a demonstration of the usage of this library in the flexirecord-demo.rb file (module FlexiRecordDemo).
32
+
33
+ module FlexiRecord
34
+
35
+
36
+ # Quoted table alias used in SQL for the object to be selected.
37
+ DefaultTableAlias = '"obj"'.freeze
38
+
39
+ # Quoted table alias used in SQL for relationship entries in many-to-many relations.
40
+ RelationshipTableAlias = '"rel"'.freeze
41
+
42
+ # Property of objects fetched through many-to-many relations, which contain the relationship object.
43
+ RelationshipColumn = 'rel'.freeze
44
+
45
+
46
+ # Thrown when a database error occurs (e.g. constraint violation).
47
+
48
+ class DatabaseError < StandardError
49
+ end
50
+
51
+
52
+ # Transaction isolation levels are represented by (constant) IsolationLevel objects:
53
+ # - IsolationLevel::ReadUncommitted
54
+ # (Data written by concurrent uncommitted transactions may be read.)
55
+ # - IsolationLevel::ReadCommitted
56
+ # (Only data, which has been committed by other transactions is read.)
57
+ # - IsolationLevel::RepeatableRead
58
+ # - IsolationLevel::Serializable
59
+ # (The first query inside a transaction generates a "snapshot" of the database, which is then used for following read accesses.)
60
+
61
+ class IsolationLevel
62
+
63
+ include Comparable
64
+ private_class_method :new
65
+ @@symbols = {}
66
+
67
+ # Returns an IsolationLevel object, matching one of these symbols:
68
+ # - :read_uncommitted
69
+ # - :read_committed
70
+ # - :repeatable_read
71
+ # - :serializable
72
+ def self.by_symbol(symbol)
73
+ @@symbols[symbol.to_sym]
74
+ end
75
+
76
+ # Used for generating the 4 constants representing the possible isolation levels.
77
+ def initialize(integer, symbol, sql, name)
78
+ @integer = integer.to_i
79
+ @symbol = symbol.to_sym
80
+ @sql = sql.to_s.dup.freeze
81
+ @name = name.to_s.dup.freeze
82
+ @@symbols[@symbol] = self
83
+ nil
84
+ end
85
+
86
+ # Returns the SQL string representation of the isolation level.
87
+ def to_s
88
+ @sql
89
+ end
90
+
91
+ # Returns an integer representing the isolation level, which is also used for comparing/ordering them.
92
+ def to_i
93
+ @integer
94
+ end
95
+
96
+ # Returns the name of the constant referring to the isolation level.
97
+ def inspect
98
+ "#{self.class.name}::#{@name}"
99
+ end
100
+
101
+ # Compares the isolation level with another.
102
+ # (Isolation levels providing fewer isolation are considered smaller.)
103
+ def <=>(other)
104
+ self.to_i <=> other.to_i
105
+ end
106
+
107
+ ReadUncommitted = new(0, :read_uncommitted, "READ UNCOMMITTED", "ReadUncommitted")
108
+ ReadCommitted = new(1, :read_committed, "READ COMMITTED", "ReadCommitted")
109
+ RepeatableRead = new(2, :repeatable_read, "REPEATABLE READ", "RepeatableRead")
110
+ Serializable = new(3, :serializable, "SERIALIZABLE", "Serializable")
111
+
112
+ end # end of class IsolationLevel
113
+
114
+
115
+ # Objects of this class are used to describe a reference between two tables. You can create and register them by calling BaseRecord::add_many_to_one_reference or RaseRecord::add_one_to_one_reference.
116
+
117
+ class Reference
118
+
119
+ private_class_method :new
120
+
121
+ # Returns a new reference object, describing a relation where objects of the 'source_class' refer to objects of the 'destination_class'. The 'column_info' field describes the columns used for that reference. If the 'column_info' field is a string, the primary key is used in the destination class, and the primary key prefixed by the string given in 'column_info' is used as the foreign key in the source class. If 'column_info' is an array, it contains the columns in the source class, followed by the columsn in the destination class. The field 'src_to_dst_column' contains the name of the column in the source class, which is referring to one object of the destination class. The field 'dst_to_src_column' contains the name of the column in the destination class, which is referring to one or many objects of the source class. After the reference has been created reader, loader and setter functions are added to the 'source_class' and 'destination_class', to provide access to referenced and referring objects.
122
+ # This method is private, use one of the child classes OneToOneReference or ManyToOneReference, which get the same arguments, to generate references of a given type.
123
+ def initialize(source_class, destination_class, column_info, src_to_dst_column, dst_to_src_column)
124
+ unless source_class.kind_of? Class and destination_class.kind_of? Class
125
+ raise TypeError, "Class expected"
126
+ end
127
+ @source_class = source_class
128
+ @destination_class = destination_class
129
+ if column_info.respond_to? :to_ary
130
+ column_info = column_info.to_ary.flatten
131
+ unless column_info.length % 2 == 0
132
+ raise ArgumentError, "Flattened 'column_info' array contains odd number of elements."
133
+ end
134
+ @source_columns = column_info[0, column_info.length / 2].collect { |column| column.to_s.dup.freeze }
135
+ @destination_columns = column_info[column_info.length / 2, column_info.length / 2].collect { |column| column.to_s.dup.freeze }
136
+ elsif column_info.respond_to? :to_str
137
+ column_info = column_info.to_str
138
+ @source_columns = []
139
+ @destination_columns = []
140
+ destination_class.primary_columns.each do |column|
141
+ @source_columns << "#{column_info}#{column}".freeze
142
+ @destination_columns << column
143
+ end
144
+ else
145
+ raise ArgumentError, "Array or String expected"
146
+ end
147
+ @source_columns.freeze
148
+ @destination_columns.freeze
149
+ @src_to_dst_column = src_to_dst_column.to_s.dup.freeze
150
+ @dst_to_src_column = dst_to_src_column.to_s.dup.freeze
151
+ # Work at the source class:
152
+ @source_class.set_loader(@src_to_dst_column) do |source_records, arguments|
153
+ unless arguments.empty?
154
+ raise ArgumentError, "No extra arguments may be specified for outgoing reference columns."
155
+ end
156
+ destination_records = @destination_class.select_by_value_set(
157
+ @destination_columns,
158
+ source_records.collect { |source_record|
159
+ @source_columns.collect { |column| source_record[column] }
160
+ },
161
+ *arguments
162
+ )
163
+ destination_record_hash = {}
164
+ destination_records.each do |destination_record|
165
+ destination_record_hash[@destination_columns.collect { |column| destination_record[column] }] = destination_record
166
+ if self.one_to_one?
167
+ destination_record[@dst_to_src_column] = nil
168
+ end
169
+ end
170
+ source_records.each do |source_record|
171
+ destination_record =
172
+ destination_record_hash[@source_columns.collect { |column| source_record[column] }]
173
+ source_record[@src_to_dst_column, *arguments] = destination_record
174
+ if destination_record and self.one_to_one?
175
+ destination_record[@dst_to_src_column] = source_record
176
+ end
177
+ end
178
+ next destination_records
179
+ end
180
+ @source_columns.each_index do |column_index|
181
+ source_column = @source_columns[column_index]
182
+ destination_column = @destination_columns[column_index]
183
+ @source_class.set_reader(source_column) do |source_record, arguments|
184
+ destination_record = source_record[@src_to_dst_column]
185
+ next destination_record ? destination_record[destination_column] : source_record[source_column]
186
+ end
187
+ @source_class.set_setter(source_column) do |source_record, value|
188
+ source_record.delete_from_cache(@src_to_dst_column)
189
+ source_record[source_column] = value
190
+ end
191
+ end
192
+ if self.one_to_one?
193
+ @source_class.set_setter(@src_to_dst_column) do |source_record, value|
194
+ old_destination_record = source_record[@src_to_dst_column]
195
+ if old_destination_record
196
+ old_destination_record[@dst_to_src_column] = nil
197
+ end
198
+ source_record[@src_to_dst_column] = value
199
+ if value
200
+ value[@dst_to_src_column] = source_record
201
+ end
202
+ end
203
+ end
204
+ # Work at the destination class:
205
+ @destination_class.set_loader(@dst_to_src_column) do |destination_records, arguments|
206
+ unless arguments.empty?
207
+ unless arguments[0].respond_to? :to_str
208
+ raise "First argument of reader method is not a SQL snippet string."
209
+ end
210
+ arguments[0] = arguments[0].to_str
211
+ end
212
+ source_records = @source_class.select_by_value_set(
213
+ @source_columns,
214
+ destination_records.collect { |destination_record|
215
+ @destination_columns.collect { |column| destination_record[column] }
216
+ },
217
+ *arguments
218
+ )
219
+ destination_record_hash = {}
220
+ destination_records.each do |destination_record|
221
+ destination_record_hash[@destination_columns.collect { |column| destination_record[column] }] = destination_record
222
+ destination_record[@dst_to_src_column, *arguments] =
223
+ if self.many_to_one?
224
+ FlexiRecord::RecordArray.new(@source_class)
225
+ else
226
+ nil
227
+ end
228
+ end
229
+ source_records.each do |source_record|
230
+ destination_record = destination_record_hash[@source_columns.collect { |column| source_record[column] }]
231
+ source_record[@src_to_dst_column] = destination_record
232
+ if destination_record
233
+ if self.many_to_one?
234
+ destination_record[@dst_to_src_column, *arguments] << source_record
235
+ else
236
+ destination_record[@dst_to_src_column, *arguments] = source_record
237
+ end
238
+ end
239
+ end
240
+ next source_records
241
+ end
242
+ if self.one_to_one?
243
+ @destination_class.set_setter(@dst_to_src_column) do |destination_record, value|
244
+ old_source_record = destination_record[@dst_to_src_column]
245
+ if old_source_record
246
+ old_source_record[@src_to_dst_column] = nil
247
+ end
248
+ destination_record[@dst_to_src_column] = value
249
+ if value
250
+ value[@src_to_dst_column] = destination_record
251
+ end
252
+ end
253
+ end
254
+ return self
255
+ end
256
+
257
+ # Combines two ManyToOneReference's to a many-to-many relation. 'other_reference' is another Reference object (as returned by Reference#new, BaseRecord#add_many_to_one_reference or BaseRecord#add_one_to_one_reference), 'own_column' is the virtual column to be installed in the destination_class of this object and 'other_column' is the virtual column to be installed in the destination_class of the 'other_reference' object.
258
+ def combine(other_reference, own_column, other_column)
259
+ unless other_reference.kind_of? FlexiRecord::Reference
260
+ raise TypeError, "Object of class FlexiRecord::Reference expected."
261
+ end
262
+ reference1 = self
263
+ reference2 = other_reference
264
+ column1 = own_column.to_s
265
+ column2 = other_column.to_s
266
+ unless self.source_class == other_reference.source_class
267
+ "Combining references having different source classes is not possible."
268
+ end
269
+ relationship_class = self.source_class
270
+ [
271
+ [reference1, reference2, column1, column2],
272
+ [reference2, reference1, column2, column1]
273
+ ].each do |source_reference, destination_reference, source_column, destination_column|
274
+ source_class = source_reference.destination_class
275
+ destination_class = destination_reference.destination_class
276
+ tmp1 = []
277
+ destination_reference.source_columns.each_index do |column_index|
278
+ tmp1 << [
279
+ destination_reference.source_columns[column_index],
280
+ destination_reference.destination_columns[column_index]
281
+ ]
282
+ end
283
+ source_class.set_loader destination_column do |source_records, arguments|
284
+ sql_arguments = arguments.dup
285
+ sql_snippet = sql_arguments.shift
286
+ unless sql_snippet.nil?
287
+ unless sql_snippet.respond_to? :to_str
288
+ raise "First argument of reader method is not a SQL snippet string."
289
+ end
290
+ sql_snippet = sql_snippet.to_str
291
+ end
292
+ # TODO: Do SELECT DISTINCT. (breaks for now, as ORDER BY expressions must appear in select list)
293
+ destination_records = unless source_records.empty?
294
+ destination_class.sql(
295
+ 'SELECT ' << FlexiRecord::DefaultTableAlias << '.* FROM (' <<
296
+ 'SELECT "obj".*, ' << relationship_class.columns.collect { |column| '"rel"."' << column << '" AS "_flexirecord_rel_' << column << '"' }.join(', ') << ' FROM ' << relationship_class.table << ' "rel" JOIN ' << destination_class.table << ' "obj" ON ' << tmp1.collect { |tmp1a, tmp1b| '"rel"."' << tmp1a << '" = "obj"."' << tmp1b << '"' }.join(' AND ') << ' WHERE (' << source_reference.source_columns.collect { |column| '"rel"."' << column << '"' }.join(', ') << ') IN (' << source_records.collect { |record| '(' << source_reference.source_columns.collect { '$' }.join(', ') << ')' }.join(', ') << ')' <<
297
+ ') AS ' << FlexiRecord::DefaultTableAlias << ' JOIN ' << relationship_class.table << ' ' << FlexiRecord::RelationshipTableAlias << ' ON ' << relationship_class.primary_columns.collect { |column| '' << FlexiRecord::RelationshipTableAlias << '."' << column << '" = ' << FlexiRecord::DefaultTableAlias << '."_flexirecord_rel_' << column << '"' }.join(' AND ') << ' ' << sql_snippet.to_s,
298
+ *(source_records.collect { |record| source_class.primary_columns.collect { |column| record.read(column) } } + sql_arguments)
299
+ )
300
+ else
301
+ FlexiRecord::RecordArray.new(destination_class)
302
+ end
303
+ destination_record_hash = {}
304
+ destination_records.each do |destination_record|
305
+ (destination_record_hash[
306
+ source_reference.source_columns.collect { |column|
307
+ destination_record['_flexirecord_rel_' << column]
308
+ }
309
+ ] ||= FlexiRecord::RecordArray.new(destination_class)) << destination_record
310
+ relationship_hash = { destination_reference.src_to_dst_column => destination_record }
311
+ relationship_class.columns.each do |column|
312
+ relationship_hash[column] =
313
+ destination_record.delete_from_cache("_flexirecord_rel_#{column}")
314
+ end
315
+ destination_record[FlexiRecord::RelationshipColumn] = relationship_class.new(relationship_hash)
316
+ end
317
+ source_records.each do |source_record|
318
+ source_record[destination_column, *arguments] = (destination_record_hash[source_reference.destination_columns.collect { |column| source_record[column] } ]) || FlexiRecord::RecordArray.new(destination_class)
319
+ end
320
+ next destination_records
321
+ end
322
+ end
323
+ end
324
+
325
+ # Class, whose objects are referring to others.
326
+ attr_reader :source_class
327
+
328
+ # Class, whose objects are referred by others.
329
+ attr_reader :destination_class
330
+
331
+ # Columns in the referring class, providing the foreign key.
332
+ attr_reader :source_columns
333
+
334
+ # Columns in the referred class, providing a unique or primary key.
335
+ attr_reader :destination_columns
336
+
337
+ # Name (String) of the column in the source class, which is referring to one object of the destination class.
338
+ attr_reader :src_to_dst_column
339
+
340
+ # Name (String) of the column in the destination class, which is referring to one or many objects of the source class.
341
+ attr_reader :dst_to_src_column
342
+
343
+ # Returns true, if the object describes a one-to-one relation.
344
+ def one_to_one?
345
+ @one_to_one
346
+ end
347
+
348
+ # Returns true, if the object describes a many-to-one relation.
349
+ def many_to_one?
350
+ not @one_to_one
351
+ end
352
+
353
+ end # end of class Reference
354
+
355
+
356
+ # One-to-one reference. See FlexiRecord::Reference for details.
357
+
358
+ class OneToOneReference < FlexiRecord::Reference
359
+
360
+ public_class_method :new
361
+
362
+ # See FlexiRecord::Reference.new for details.
363
+ def initialize(*arguments)
364
+ super
365
+ @one_to_one = true
366
+ end
367
+
368
+ end # end of class OneToOneReference
369
+
370
+
371
+ # Many-to-one reference. See FlexiRecord::Reference for details.
372
+
373
+ class ManyToOneReference < FlexiRecord::Reference
374
+
375
+ public_class_method :new
376
+
377
+ # See FlexiRecord::Reference.new for details.
378
+ def initialize(*arguments)
379
+ super
380
+ @one_to_one = false
381
+ end
382
+
383
+ end # end of class ManyToOneReference
384
+
385
+
386
+ # An abstract record, super-class of all record classes. By now there is only one sub-class 'BaseRecord', which represents a record being directly stored in one database table. Other sub-classes might be added in near future, to be able to use inheritance of data models to be stored in the database using multiple tables in the backend for one model.
387
+
388
+ class AbstractRecord
389
+
390
+ private_class_method :new
391
+
392
+ end # end of class AbstractRecord
393
+
394
+
395
+ # Sub-class of Array to provide special methods to be used on multiple database records at once.
396
+
397
+ class RecordArray < Array
398
+
399
+ # Creates a new Array to hold objects of type 'record_class'. Additional arguments will be passed to Array#new.
400
+ def initialize(record_class, *arguments)
401
+ super(*arguments)
402
+ @flexirecord_class = record_class
403
+ @flexirecord_preloaded = {}
404
+ end
405
+
406
+ # Returns the record class of the elements of the array.
407
+ def record_class
408
+ @flexirecord_class
409
+ end
410
+
411
+ # Preloads a virtualized column of an Array of records at once (without doing one SQL query for each record). Can return another RecordArray, which can be used for the next level of preloading.
412
+ def preload(column, *arguments)
413
+ column = column.to_s
414
+ @flexirecord_class.prepare_read_parameters(column, arguments)
415
+ cache_key = [column] + arguments
416
+ if @flexirecord_preloaded.has_key?(cache_key)
417
+ return @flexirecord_preloaded[cache_key]
418
+ end
419
+ column = column.to_s
420
+ loader = self.record_class.loader(column)
421
+ unless loader
422
+ raise ArgumentError, "Could not preload column '#{column}', due to missing loading procedure."
423
+ end
424
+ return @flexirecord_preloaded[cache_key] = loader.call(self, arguments)
425
+ end
426
+
427
+ # Returns a RecordArray of re-selected objects. This can be used to re-sort records by the database, to do further filtering, or to reload all records at once. The result will not contain duplicates, even if there are duplicates in the receiver of the method call.
428
+ def reselect(sql_snippet=nil, *arguments)
429
+ unless self.empty?
430
+ @flexirecord_class.select_by_value_set(@flexirecord_class.primary_columns, self.collect { |record| @flexirecord_class.primary_columns.collect { |column| record.read(column) } }, sql_snippet, *arguments)
431
+ end
432
+ return self
433
+ end
434
+
435
+ end # end of class RecordArray
436
+
437
+
438
+ # A record representing a row of a database table or query result.
439
+
440
+ class BaseRecord < FlexiRecord::AbstractRecord
441
+
442
+ include MonitorMixin
443
+
444
+ public_class_method :new
445
+
446
+ class << self # singleton meta class of BaseRecord
447
+
448
+ # Sets the database schema name for this class.
449
+ def schema_name=(schema_name)
450
+ @schema_name = schema_name ? schema_name.to_s.dup.freeze : nil
451
+ end
452
+
453
+ # Sets the database table name for this class. This must only be used on sub-classes of BaseRecord, and never on BaseRecord itself.
454
+ def table_name=(table_name)
455
+ @table_name = table_name ? table_name.to_s.dup.freeze : nil
456
+ end
457
+
458
+ # Returns the database schema name of this class (or of it's superclass, if it has no own schema name)
459
+ def schema_name
460
+ @schema_name or (
461
+ (superclass <= FlexiRecord::BaseRecord) ?
462
+ superclass.schema_name :
463
+ nil
464
+ )
465
+ end
466
+
467
+ # Returns the database schema name, even if no schema is set (in this case "public" is returned.
468
+ def schema_name!
469
+ schema_name or "public".freeze
470
+ end
471
+
472
+ # Returns the table name of this class (or of it's superclass, if it has no own table name)
473
+ def table_name
474
+ @table_name or (
475
+ (superclass <= FlexiRecord::BaseRecord) ?
476
+ superclass.table_name :
477
+ nil
478
+ )
479
+ end
480
+
481
+ # Returns the table name, or raises an error, if no name is found.
482
+ def table_name!
483
+ table_name or
484
+ raise "No table name set for #{self.name}."
485
+ end
486
+
487
+ # Returns an SQL snippet including the quoted schema and table name.
488
+ def table
489
+ schema_name ?
490
+ %Q("#{schema_name}"."#{table_name!}") :
491
+ %Q("#{table_name!}")
492
+ end
493
+
494
+ # Sets the connection pool to use for this class in general.
495
+ def connection_pool=(pool)
496
+ @connection_pool = pool
497
+ end
498
+
499
+ # Returns the connection pool being used for this class in general.
500
+ def connection_pool
501
+ @connection_pool
502
+ end
503
+
504
+ # Sets the ConnectionPool to use for this class in the current thread.
505
+ def thread_connection_pool=(pool)
506
+ pool_hash = Thread.current[:flexirecord_thread_connection_pools] ||= {}
507
+ if pool.nil?
508
+ pool_hash.delete(self)
509
+ else
510
+ pool_hash[self] = pool
511
+ end
512
+ nil
513
+ end
514
+
515
+ # Returns the ConnectionPool being used for the current thread, if an explicit pool was set for the current thread.
516
+ def thread_connection_pool
517
+ (Thread.current[:flexirecord_thread_connection_pools] ||= {})[self]
518
+ end
519
+
520
+ # Calls the given block with a Connection object to the database being used to store objects of this class.
521
+ def use_connection
522
+ pool = nil
523
+ catch :found do
524
+ current_class = self
525
+ while current_class <= FlexiRecord::BaseRecord
526
+ throw :found if pool = current_class.thread_connection_pool
527
+ current_class = current_class.superclass
528
+ end
529
+ current_class = self
530
+ while current_class <= FlexiRecord::BaseRecord
531
+ throw :found if pool = current_class.connection_pool
532
+ current_class = current_class.superclass
533
+ end
534
+ raise "No connection pool set for #{self.name}."
535
+ end
536
+ pool.use_connection do |connection|
537
+ return yield(connection)
538
+ end
539
+ end
540
+
541
+ # Returns true, if a transaction is in progress on the connection used for accessing the table of this class.
542
+ def transaction?
543
+ use_connection do |connection|
544
+ return connection.transaction?
545
+ end
546
+ end
547
+
548
+ # Returns the isolation_level of a transaction in progress on the connection used for accessing the table of this class.
549
+ def isolation_level
550
+ use_connection do |connection|
551
+ return connection.isolation_level
552
+ end
553
+ end
554
+
555
+ # Wraps the given block in a transaction of the database being used to store objects of this class. See FlexiRecord::Connection#transaction for details of the command.
556
+ def transaction(*arguments)
557
+ use_connection do |connection|
558
+ connection.transaction(*arguments) do
559
+ return yield
560
+ end
561
+ end
562
+ result
563
+ end
564
+
565
+ # Autodetects the columns (and primary key columns) of the underlaying table, to be later retrieved by the 'columns' and the 'primary_columns' methods.
566
+ def autodetect_columns
567
+ columns = []
568
+ primary_columns = []
569
+ db_query('SELECT ' <<
570
+ '"pg_attribute"."attname", ' <<
571
+ '"pg_constraint"."conkey" @> ARRAY["pg_attribute"."attnum"] AS "primary" ' <<
572
+ 'FROM "pg_attribute" ' <<
573
+ 'JOIN "pg_class" ON "pg_attribute"."attrelid" = "pg_class"."oid" ' <<
574
+ 'JOIN "pg_namespace" ON "pg_class"."relnamespace" = "pg_namespace"."oid" ' <<
575
+ 'LEFT JOIN "pg_constraint" ON "pg_class"."oid" = "pg_constraint"."conrelid" ' <<
576
+ 'WHERE "pg_attribute"."attnum" > 0 ' <<
577
+ 'AND "pg_class"."relname" = $ ' <<
578
+ 'AND "pg_namespace"."nspname" = $ ' <<
579
+ 'AND "pg_constraint"."contype" = $ ' <<
580
+ 'ORDER BY "attnum"',
581
+ table_name!, schema_name!, 'p').each do |attribute_record|
582
+ attribute_record.attname.freeze
583
+ columns << attribute_record.attname
584
+ primary_columns << attribute_record.attname if attribute_record.primary
585
+ end
586
+ @primary_columns = primary_columns.freeze
587
+ @columns = columns.freeze
588
+ nil
589
+ end
590
+
591
+ # Returns an array of columns (String's) of the underlaying table. The columns may be autodetected (and cached) at the first call of this method.
592
+ def columns
593
+ return [].freeze if table_name.nil?
594
+ autodetect_columns if @columns.nil?
595
+ return @columns
596
+ end
597
+
598
+ # Returns an array of columns (String's) being part of the primary key of the underlaying table. (This will be in most cases an Array with one entry.) The columns may be autodetected (and cached) at the first call of this method.
599
+ def primary_columns
600
+ return [].freeze if table_name.nil?
601
+ autodetect_columns if @primary_columns.nil?
602
+ return @primary_columns
603
+ end
604
+
605
+ # This method is used on each Array of records being selected from the database via the 'sql' or 'select' method. It does nothing, but can be extended to automatically preload certain fields for example.
606
+ def after_select(records)
607
+ end
608
+
609
+ # Executes the given SQL query with optional arguments on the database being used to store objects of this class. Returns an array of objects of the class this method is used on (containing the data of all rows of the query result).
610
+ def sql(command_template, *command_arguments)
611
+ records = nil
612
+ if command_template
613
+ transaction(:unless_open, :serializable) do
614
+ use_connection do |connection|
615
+ records = connection.record_query(self, command_template, *command_arguments)
616
+ end
617
+ after_select(records)
618
+ end
619
+ else
620
+ records = FlexiRecord::RecordArray.new(self)
621
+ after_select(records)
622
+ end
623
+ return records
624
+ end
625
+
626
+ # Executes the given SQL query with optional arguments on the database being used to store objects of this class. Returns a single object of the class this method is used on (containing the data of the first row of the query result.)
627
+ def sql1(*arguments)
628
+ sql(*arguments).first
629
+ end
630
+
631
+ # Executes the given SQL query with optional arguments on the database being used to store objects of this class. Returns an array of BaseRecord's (but NOT sub-classes of BaseRecord) containing the data of all rows of the query result.
632
+ def db_query(command_template, *command_arguments)
633
+ use_connection do |connection|
634
+ return connection.query(command_template, *command_arguments)
635
+ end
636
+ end
637
+
638
+ # Executes the given SQL query with optional arguments on the database being used to store objects of this class. Returns an array of BaseRecord's (but NOT sub-classes of BaseRecord) containing the data of the first row of the query result.
639
+ def db_query1(*arguments)
640
+ db_query(*arguments).first
641
+ end
642
+
643
+ # Executes the given SQL command with optional arguments on the database being used to store objects of this class. Returns nil.
644
+ def db_execute(command_template, *command_arguments)
645
+ use_connection do |connection|
646
+ return connection.execute(command_template, *command_arguments)
647
+ end
648
+ end
649
+
650
+ # Wrapper for the 'sql' method including already a part of the SQL select command. Please see the source code to understand how this method works. If there is a primary key, the selection will not contain duplicates, even if there have been JOINs with other tables.
651
+ def select(sql_snippet=nil, *arguments)
652
+ sql("SELECT#{(sql_snippet.nil? or self.primary_columns.empty?) ? '' : ' DISTINCT'} #{FlexiRecord::DefaultTableAlias}.* FROM #{table} #{FlexiRecord::DefaultTableAlias}#{sql_snippet ? ' ' : ''}#{sql_snippet}", *arguments)
653
+ end
654
+
655
+ # Same as 'select', but returns only the first member of the Array, or nil.
656
+ def select1(*arguments)
657
+ select(*arguments).first
658
+ end
659
+
660
+ # Executes an SQL query, selecting rows matching a given set of keys and values, optionally appending a given SQL snippet with parameters. Returns an Array of records. If there is a primary key, the selection will not contain duplicates, even if there have been JOINs with other tables.
661
+ def select_by_value_set(keys, set_of_values, sql_snippet=nil, *arguments)
662
+ flattened_values = set_of_values.to_ary.flatten
663
+ if flattened_values.empty?
664
+ return sql(nil)
665
+ else
666
+ if sql_snippet
667
+ return sql(
668
+ 'SELECT ' << (self.primary_columns.empty? ? '' : 'DISTINCT ') << FlexiRecord::DefaultTableAlias << '.* FROM (' <<
669
+ 'SELECT * FROM ' << table << ' WHERE (' << keys.collect { |key| '"' << key << '"' }.join(', ') << ') ' << 'IN (' << set_of_values.collect { |values| '(' << values.collect { |value| '$' }.join(', ') << ')' }.join(', ') << ')' <<
670
+ ') AS ' << FlexiRecord::DefaultTableAlias << ' ' << sql_snippet.to_s,
671
+ *(flattened_values + arguments)
672
+ )
673
+ else
674
+ return sql(
675
+ 'SELECT' << (self.primary_columns.empty? ? '' : ' DISTINCT') << ' * FROM ' << table << ' WHERE (' << keys.collect { |key| '"' << key << '"' }.join(', ') << ') ' << 'IN (' << set_of_values.collect { |values| '(' << values.collect { |value| '$' }.join(', ') << ')' }.join(', ') << ')',
676
+ *(flattened_values + arguments)
677
+ )
678
+ end
679
+ end
680
+ end
681
+
682
+ # Adds a "shortcut" for a parameter to reader and loader functions.
683
+ #
684
+ # Example: List.add_read_option :items, :by_name, 'ORDER BY "name"'
685
+ def add_read_option(column, symbol, value)
686
+ unless symbol.kind_of? Symbol
687
+ raise "Symbol expected as second argument to 'add_read_option'."
688
+ end
689
+ (@read_options ||= {})[[column.to_s, symbol]] = value
690
+ end
691
+
692
+ # Returns the value of a "shortcut" for a parameter to reader and loader functions.
693
+ def read_option_value(column, symbol)
694
+ value = (@read_options || {})[[column.to_s, symbol]] || ((superclass <= FlexiRecord::BaseRecord) ? superclass.read_option_value(column, symbol) : nil)
695
+ end
696
+
697
+ # Modifies a parameter array by replacing "shortcut" symbols being defined by add_read_option. Returns the parameter array.
698
+ def prepare_read_parameters(column, parameters)
699
+ option = parameters.first
700
+ value = read_option_value(column, option.nil? ? :default : option)
701
+ if value
702
+ parameters[0] = value
703
+ elsif option == :default
704
+ parameters.shift
705
+ end
706
+ return parameters
707
+ end
708
+
709
+ # Sets a given block to be the "reader function" for a particlular virtualized column. A reader function is invoked, when you try to read a value of the given 'column' of a record. Two arguments are passed to the block. The first is the record, whose data is to be read, the second is an Array of arguments passed to the method used for reading the value. The block has to evaluate to the value which should be read.
710
+ def set_reader(column, &reader)
711
+ (@readers ||= {})[column.to_s] = reader
712
+ end
713
+
714
+ # Returns the reader function (a Proc object) for a certain virtualized column.
715
+ def reader(column)
716
+ (@readers || {})[column.to_s] or ((superclass <= FlexiRecord::BaseRecord) ? superclass.reader(column) : nil)
717
+ end
718
+
719
+ # Returns an array containing all virtualized columns having a reader Proc stored in the class.
720
+ def reader_columns
721
+ (@readers || {}).keys + (
722
+ (superclass <= FlexiRecord::BaseRecord) ?
723
+ superclass.reader_columns :
724
+ []
725
+ )
726
+ end
727
+
728
+ # Sets a given block to be the "loader function" for a particular virtualized column. Loader functions are invoked, when you try to read an uncached value of the given 'column' of a record, or when you preload data for a whole Array of records. Two arguments are passed to the block. The first is an Array of records, whose data is to be loaded, the second is an Array of arguments passed to the method used for accessing the value. The block has to evaluate to an Array of records, which can be used for more than one level deep preloads.
729
+ def set_loader(column, &loader)
730
+ (@loaders ||= {})[column.to_s] = loader
731
+ end
732
+
733
+ # Returns the loader function (a Proc object) for a certain virtualized column.
734
+ def loader(column)
735
+ (@loaders || {})[column.to_s] or ((superclass <= FlexiRecord::BaseRecord) ? superclass.loader(column) : nil)
736
+ end
737
+
738
+ # Sets a given block to be the "setter function" for a particular virtualized column. The setter function is invoked, when you set the value of the given 'column' of a record. Two arguments are passed to the block. The first is the record, whose data is to be changed, the second is the new value to be written into the 'column' field.
739
+ def set_setter(column, &setter)
740
+ (@setters ||= {})[column.to_s] = setter
741
+ end
742
+
743
+ # Returns the setter function (a Proc object) for a certain virtualized column.
744
+ def setter(column)
745
+ (@setters || {})[column.to_s] or ((superclass <= FlexiRecord::BaseRecord) ? superclass.setter(column) : nil)
746
+ end
747
+
748
+ # Adds an OneToOneReference to the class (by simply creating it). The first argument is the destination class, followed by arguments being passed to Reference.new.
749
+ def add_one_to_one_reference(destination_class, *arguments)
750
+ return FlexiRecord::OneToOneReference.new(
751
+ self, destination_class, *arguments
752
+ )
753
+ end
754
+
755
+ # Adds a ManyToManyReference to the class (by simply creating it). The first argument is the destination class, followed by arguments being passed to Reference.new.
756
+ def add_many_to_one_reference(destination_class, *arguments)
757
+ return FlexiRecord::ManyToOneReference.new(
758
+ self, destination_class, *arguments
759
+ )
760
+ end
761
+
762
+ end # end of singleton meta class of BaseRecord
763
+
764
+ private # methods of BaseRecord
765
+
766
+ # Saves a copy of the values of the primary key in the @old_primary_key Array.
767
+ def copy_primary_key
768
+ synchronize do
769
+ @old_primary_key.clear
770
+ self.class.primary_columns.each do |column|
771
+ @old_primary_key[column] = self.read(column)
772
+ end
773
+ end
774
+ nil
775
+ end
776
+
777
+ # Creates a new record object with the given keys and values in the 'data' hash. If 'saved' is true, it is considered to be existent in the database already, if 'saved' is false (default), it is considered to be a new object, which has not been saved.
778
+ def initialize(data={}, saved=false)
779
+ super()
780
+ @data_hash = {}
781
+ self.update(data)
782
+ @saved = saved ? true : false
783
+ @old_primary_key = {}
784
+ copy_primary_key if @saved and self.class.table_name
785
+ nil
786
+ end
787
+
788
+ protected # methods of BaseRecord
789
+
790
+ # Helper method for dup and replace. Do not use directly.
791
+ def dup_internal_state
792
+ @data_hash = @data_hash.dup
793
+ @old_primary_key = @old_primary_key.dup
794
+ nil
795
+ end
796
+
797
+ # Helper mthod for dup and replace. Do not use directly.
798
+ def read_internal_state
799
+ return @data_hash.dup, @saved, @old_primary_key.dup
800
+ end
801
+
802
+ public # methods of BaseRecord
803
+
804
+ # Rewrites several attributes given by the keys and values in the 'data' hash. Returns self.
805
+ def update(data)
806
+ synchronize do
807
+ data.each { |key, value| self.set(key, value) }
808
+ return self
809
+ end
810
+ end
811
+
812
+ # Duplicates a record, including it's internal state.
813
+ def dup
814
+ synchronize do
815
+ duplicate = super
816
+ duplicate.dup_internal_state
817
+ return duplicate
818
+ end
819
+ end
820
+
821
+ # Replaces the internal state with the state of a backup. This method is needed for transaction rollbacks.
822
+ def replace(backup)
823
+ synchronize do
824
+ raise TypeError, "Can not restore backup of objects of other classes." unless backup.class == self.class
825
+ @data_hash, @saved, @old_primary_key = backup.read_internal_state
826
+ return self
827
+ end
828
+ end
829
+
830
+ # Reads a value (whose key can consist of multiple fields) from the internal cache. The first argument is by convention a name of a column as String(!), but NOT as a Symbol.
831
+ def [](*key)
832
+ synchronize do
833
+ return @data_hash[key]
834
+ end
835
+ end
836
+
837
+ # Writes a value to the internal cache. The first argument is by convention a name of a column as String(!), but NOT as a Symbol.
838
+ def []=(*arguments)
839
+ synchronize do
840
+ value = arguments.pop
841
+ key = arguments
842
+ return @data_hash[key] = value
843
+ end
844
+ end
845
+
846
+ # Returns true, if the internal cache has stored the specified entry, otherwise false.
847
+ def has_key?(*key)
848
+ synchronize do
849
+ return @data_hash.has_key?(key)
850
+ end
851
+ end
852
+
853
+ # Deletes an entry from the internal cache, and returns it's value.
854
+ def delete_from_cache(*key)
855
+ synchronize do
856
+ return @data_hash.delete(key)
857
+ end
858
+ end
859
+
860
+ # Reads the field with the specified 'column'. If there is a dynamic reader, this reader is used, otherwise an existent assigned loader function is invoked or the internal cache will give a result. If there is no data for the column with the given name, then nil will be returned.
861
+ def read(column, *arguments)
862
+ column = column.to_s
863
+ self.class.prepare_read_parameters(column, arguments)
864
+ data_hash_key = [column] + arguments
865
+ reader = self.class.reader(column)
866
+ loader = self.class.loader(column)
867
+ synchronize do
868
+ if reader
869
+ return reader.call(self, arguments)
870
+ elsif @data_hash.has_key?(data_hash_key)
871
+ return @data_hash[data_hash_key]
872
+ elsif loader
873
+ loader.call(FlexiRecord::RecordArray.new(self.class, [self]), arguments)
874
+ unless @data_hash.has_key?(data_hash_key)
875
+ raise "Record loader failed."
876
+ end
877
+ end
878
+ return @data_hash[data_hash_key]
879
+ end
880
+ end
881
+
882
+ # Sets a value of a field of the specified 'column'. If there is a dynamic setter, this setter is used, otherwise the value get's written in the internal cache.
883
+ def set(column, value)
884
+ column = column.to_s
885
+ setter = self.class.setter(column)
886
+ if setter
887
+ setter.call(self, value)
888
+ return @data_hash[[column]]
889
+ else
890
+ return @data_hash[[column]] = value
891
+ end
892
+ end
893
+
894
+ # Returns an array of strings of the columns in the backend database, which have either values in the internal cache, or which have a dynamic reader function.
895
+ def used_columns
896
+ synchronize do
897
+ return self.class.columns & (self.class.reader_columns + @data_hash.keys.reject { |key| key.length > 1 }.collect { |key| key.first })
898
+ end
899
+ end
900
+
901
+ # Returns a string representation of the record for debugging purposes.
902
+ def inspect
903
+ synchronize do
904
+ processed_objects = Thread.current[:flexirecord_baserecord_inspect_cycle_check] ||= {}
905
+ if processed_objects[self]
906
+ return "#<#{self.class}:0x#{sprintf "%08x", object_id}"
907
+ else
908
+ begin
909
+ processed_objects[self] = true
910
+ return "#<#{self.class}:0x#{sprintf "%08x", object_id} #{@saved ? 'saved' : 'unsaved'}, old_primary_key = {" <<
911
+ self.class.primary_columns.dup.delete_if { |column| not @old_primary_key.has_key?(column) }.
912
+ collect { |column| column.inspect << '=>' << @old_primary_key[column].inspect }.join(', ') << "}, data = {" <<
913
+ self.class.columns.dup.delete_if { |column| not (@data_hash.has_key?([column]) or self.class.reader(column)) }.
914
+ collect { |column| column.inspect << '=>' << read(column).inspect }.join(', ') << "}>"
915
+ ensure
916
+ processed_objects.delete(self)
917
+ end
918
+ end
919
+ end
920
+ end
921
+
922
+ # Alias for the inspect method.
923
+ def to_s
924
+ inspect
925
+ end
926
+
927
+ undef_method :id
928
+ # Provides easy access to the fields of the record.
929
+ def method_missing(method_symbol, *arguments)
930
+ synchronize do
931
+ column = method_symbol.to_s
932
+ if column[-1, 1] == '='
933
+ column = column[0, column.length-1]
934
+ mode = :write
935
+ value = arguments.pop
936
+ else
937
+ mode = :read
938
+ end
939
+ reader = self.class.reader(column)
940
+ loader = self.class.loader(column)
941
+ table_column_existent = self.class.columns.include?(column)
942
+ if mode == :write
943
+ data_hash_key = [column] + arguments
944
+ setter = self.class.setter(column)
945
+ if setter
946
+ setter.call(self, value)
947
+ return nil
948
+ elsif @data_hash.has_key?(data_hash_key) or reader or loader or table_column_existent
949
+ @data_hash[data_hash_key] = value
950
+ return nil
951
+ end
952
+ elsif mode == :read
953
+ self.class.prepare_read_parameters(column, arguments)
954
+ data_hash_key = [column] + arguments
955
+ if reader
956
+ return reader.call(self, arguments)
957
+ elsif @data_hash.has_key?(data_hash_key)
958
+ return @data_hash[data_hash_key]
959
+ elsif loader
960
+ loader.call(FlexiRecord::RecordArray.new(self.class, [self]), arguments)
961
+ unless @data_hash.has_key?(data_hash_key)
962
+ puts data_hash_key.inspect
963
+ raise "Record loader failed."
964
+ end
965
+ return @data_hash[data_hash_key]
966
+ elsif table_column_existent
967
+ unless arguments.empty?
968
+ raise ArgumentError, "Attribute getter method does not support arguments."
969
+ end
970
+ return nil
971
+ end
972
+ end
973
+ return super
974
+ end
975
+ end
976
+
977
+ # Returns true, if the record has been saved in database once, otherwise false.
978
+ def saved?
979
+ synchronize do
980
+ @saved
981
+ end
982
+ end
983
+
984
+ # Saves the record in the database, either by INSERT'ing or UPDATE'ing it.
985
+ def save
986
+ synchronize do
987
+ used_columns = self.used_columns
988
+ primary_key = nil
989
+ if @saved
990
+ if self.class.primary_columns.empty?
991
+ raise "Can not re-save a record of a table without a primary key."
992
+ end
993
+ primary_key = self.class.db_query1(
994
+ 'UPDATE ' << self.class.table <<
995
+ ' SET ' << (used_columns.collect { |column| '"' << column << '" = $' }.join(', ')) <<
996
+ ' WHERE ' << (self.class.primary_columns.collect { |column| '"' << column << '" = $' }.join(' AND ')) <<
997
+ ' RETURNING ' << (self.class.primary_columns.collect { |column| '"' << column << '"' }.join(', ')),
998
+ *(
999
+ used_columns.collect { |column| read(column) } +
1000
+ self.class.primary_columns.collect { |column| @old_primary_key[column] }
1001
+ )
1002
+ )
1003
+ else
1004
+ if used_columns.empty?
1005
+ primary_key = self.class.db_query1('INSERT INTO ' << self.class.table << ' DEFAULT VALUES' <<
1006
+ (self.class.primary_columns.empty? ? '' : (
1007
+ ' RETURNING ' << (self.class.primary_columns.collect { |column| '"' << column << '"' }.join(', '))
1008
+ )))
1009
+ else
1010
+ primary_key = self.class.db_query1(
1011
+ 'INSERT INTO ' << self.class.table <<
1012
+ ' (' << (used_columns.collect { |column| '"' << column << '"' }.join(', ')) << ')' <<
1013
+ ' VALUES (' << (used_columns.collect { |column| '$' }.join(', ')) << ')' <<
1014
+ (self.class.primary_columns.empty? ? '' : (
1015
+ ' RETURNING ' << (self.class.primary_columns.collect { |column| '"' << column << '"' }.join(', '))
1016
+ )),
1017
+ *(
1018
+ used_columns.collect { |column| read(column) }
1019
+ )
1020
+ )
1021
+ end
1022
+ @saved = true
1023
+ end
1024
+ unless primary_key.nil?
1025
+ self.class.primary_columns.each do |column|
1026
+ self.set(column, primary_key.read(column))
1027
+ end
1028
+ end
1029
+ copy_primary_key
1030
+ return self
1031
+ end
1032
+ end
1033
+
1034
+ def destroy
1035
+ if self.saved?
1036
+ self.class.db_execute('DELETE FROM ' << self.class.table <<
1037
+ ' WHERE ' << (self.class.primary_columns.collect { |column| '"' << column << '" = $' }.join(' AND ')),
1038
+ *( self.class.primary_columns.collect { |column| @old_primary_key[column] } )
1039
+ )
1040
+ @saved = false
1041
+ end
1042
+ return self
1043
+ end
1044
+
1045
+ # Reloads the record from the database. It can not be used on records, which have not been saved to the database yet.
1046
+ def reload
1047
+ synchronize do
1048
+ if self.class.primary_columns.empty?
1049
+ raise "Can not reload a record, which has no primary key."
1050
+ end
1051
+ unless self.saved?
1052
+ raise "Can not reload a record, which has not been saved yet."
1053
+ end
1054
+ reloaded_record = self.class.db_query1(
1055
+ 'SELECT * FROM ' << self.class.table <<
1056
+ 'WHERE ' << (self.class.primary_columns.collect { |column| '"' << column << '" = $' }.join(' AND ')),
1057
+ *(
1058
+ self.class.primary_columns.collect { |column| @old_primary_key[column] }
1059
+ )
1060
+ )
1061
+ if reloaded_record.nil?
1062
+ raise DatabaseError, "Could not reload data."
1063
+ end
1064
+ new_data_hash = {}
1065
+ self.class.columns.each { |column| new_data_hash[[column]] = reloaded_record.read(column) }
1066
+ @data_hash = new_data_hash
1067
+ return self
1068
+ end
1069
+ end
1070
+
1071
+ public # end of methods of BaseRecord
1072
+
1073
+ end # end of class BaseRecord
1074
+
1075
+
1076
+ # A record representing a row of a database table which is used for cross (many-to-many) relations.
1077
+
1078
+ class Relationship < FlexiRecord::BaseRecord
1079
+
1080
+ def initialize(*arguments)
1081
+ super
1082
+ self['void'] = nil
1083
+ end
1084
+
1085
+ # Alias for the (field) method 'void'. True, if the Relationship is void, and is to be removed, when calling 'save'.
1086
+ def void?
1087
+ self.void
1088
+ end
1089
+
1090
+ # Instead of UPDATEing or INSERTing a value, depending on the state of the object, it is always replaced in the database. When the special attribute 'void' is set, the record will be destroyed instead of saved.
1091
+ def save
1092
+ # TODO: improve efficiency, when a "REPLACE" command is available in PostgreSQL
1093
+ self.class.transaction(self, :read_committed) do
1094
+ self.class.db_execute "LOCK TABLE #{self.class.table} IN SHARE ROW EXCLUSIVE MODE"
1095
+ @saved =
1096
+ self.class.select_by_value_set(self.class.primary_columns, [self.class.primary_columns.collect { |column| self.read(column) }]).length > 0
1097
+ copy_primary_key
1098
+ if self.void
1099
+ return destroy
1100
+ else
1101
+ return super
1102
+ end
1103
+ end
1104
+ end
1105
+
1106
+ end # end of class Relationship
1107
+
1108
+
1109
+ # A Connection object represents a distinct connection to the database.
1110
+
1111
+ class Connection
1112
+
1113
+ include MonitorMixin
1114
+
1115
+ # Generates a new Connection object. The passed 'options' are a hash, which may contain the following keys:
1116
+ # - :engine (only :postgresql is supported)
1117
+ # - :host
1118
+ # - :port
1119
+ # - :options
1120
+ # - :db
1121
+ # - :user
1122
+ # - :pass
1123
+ # - :data_types (used for the PostgreSQL interface to supply a mapping between OID's and ruby types, will be set automatically in the hash object, if it is nil or missing)
1124
+ def initialize(options)
1125
+ super()
1126
+ options.each do |key, value|
1127
+ case key
1128
+ when :engine
1129
+ @engine = value.to_sym
1130
+ when :host
1131
+ @host = value.to_s.dup.freeze if value
1132
+ when :port
1133
+ @port = value.to_i
1134
+ when :options
1135
+ @options = options.to_s.dup.freeze if value
1136
+ when :db
1137
+ @dbname = value.to_s.dup.freeze if value
1138
+ when :user
1139
+ @login = value.to_s.dup.freeze if value
1140
+ when :pass
1141
+ @passwd = value.to_s.dup.freeze if value
1142
+ when :data_types
1143
+ @data_types = value.to_hash.dup.freeze if value
1144
+ else
1145
+ raise ArgumentError, "Unknown option '#{key}'."
1146
+ end
1147
+ end
1148
+ raise ArgumentError, "No engine selected." if @engine.nil?
1149
+ raise ArgumentError, "Engine '#{@engine}' not supported." unless @engine == :postgresql
1150
+ unless @data_types
1151
+ @data_types = {}
1152
+ options[:data_types] = {}
1153
+ connection = nil
1154
+ begin
1155
+ connection = FlexiRecord::Connection.new(options)
1156
+ connection.query('SELECT "oid", "typname" FROM "pg_type" WHERE typtype=$', 'b').each do |type_record|
1157
+ @data_types[type_record.oid.to_i] = type_record.typname.to_s.freeze
1158
+ end
1159
+ ensure
1160
+ connection.close if connection
1161
+ end
1162
+ options[:data_types] = @data_types.freeze
1163
+ end
1164
+ @backend_connection = PGconn.new(@host, @port, @options, nil, @dbname, @login, @passwd)
1165
+ @transaction_stacklevel = 0
1166
+ @isolation_level = nil
1167
+ nil
1168
+ end
1169
+
1170
+ # Executes an SQL query and returns an Array of objects of 'record_class' (which should be a sub-class of BaseRecord). The 'command_template' is an SQL statement with '$' placeholders to be replaced by the following 'command_arguments'.
1171
+ def record_query(record_class, command_template, *command_arguments)
1172
+ command = command_template.to_s.gsub(/\$([^0-9]|$)/) {
1173
+ if command_arguments.empty?
1174
+ raise ArgumentError, "Too few arguments supplied for SQL command."
1175
+ end
1176
+ command_argument = command_arguments.shift
1177
+ if command_argument.kind_of? Rational
1178
+ command_argument = command_argument.to_f
1179
+ end
1180
+ PGconn.quote(command_argument) << $1
1181
+ # argument = command_arguments.shift
1182
+ # if argument.kind_of? FlexiRecord::SqlSnippet
1183
+ # argument.to_s + $1
1184
+ # else
1185
+ # PGconn.quote(argument) << $1
1186
+ # end
1187
+ }
1188
+ raise ArgumentError, "Too many arguments supplied for SQL command." unless command_arguments.empty?
1189
+ if $flexirecord_debug_output
1190
+ $flexirecord_debug_output << "===> #{command}\n"
1191
+ end
1192
+ begin
1193
+ synchronize do
1194
+ if record_class
1195
+ backend_result = @backend_connection.async_exec(command)
1196
+ result = FlexiRecord::RecordArray.new(record_class)
1197
+ for row in 0...(backend_result.num_tuples)
1198
+ record_data = {}
1199
+ for col in 0...(backend_result.num_fields)
1200
+ value_string = backend_result.getvalue(row, col)
1201
+ record_data[backend_result.fieldname(col)] = if value_string.nil?
1202
+ nil
1203
+ else
1204
+ if @data_types
1205
+ case @data_types[backend_result.type(col)]
1206
+ when "bool" then value_string[0, 1] == 't'
1207
+ when "int2" then value_string.to_i
1208
+ when "int4" then value_string.to_i
1209
+ when "int8" then value_string.to_i
1210
+ when "text" then value_string
1211
+ when "varchar" then value_string
1212
+ when "numeric" then
1213
+ unless value_string =~ /^([0-9]*)(\.([0-9]+)?)?$/
1214
+ raise "Unexpected format for numeric data from database."
1215
+ end
1216
+ if $3
1217
+ $1.to_i + Rational($3.to_i, 10**($3.length))
1218
+ else
1219
+ $1.to_i
1220
+ end
1221
+ else
1222
+ value_string
1223
+ end
1224
+ else
1225
+ value_string
1226
+ end
1227
+ end
1228
+ end
1229
+ result << record_class.new(record_data, true)
1230
+ end
1231
+ return result
1232
+ else
1233
+ @backend_connection.async_exec(command)
1234
+ return nil
1235
+ end
1236
+ end
1237
+ rescue PGError
1238
+ raise FlexiRecord::DatabaseError, $!.message
1239
+ end
1240
+ end
1241
+
1242
+ # Same as record_query, but using BaseRecord's as 'record_class'.
1243
+ def query(command_template, *command_arguments)
1244
+ record_query(FlexiRecord::BaseRecord, command_template, *command_arguments)
1245
+ end
1246
+
1247
+ # Same as record_query, but having no 'record_class', theirfor returning nil.
1248
+ def execute(command_template, *command_arguments)
1249
+ record_query(nil, command_template, *command_arguments)
1250
+ end
1251
+
1252
+ # Returns true, if a transaction is in progress on this connection.
1253
+ def transaction?
1254
+ synchronize do
1255
+ @transaction_stacklevel > 0
1256
+ end
1257
+ end
1258
+
1259
+ # Returns the isolation_level of a transaction in progress on this connection.
1260
+ def isolation_level
1261
+ synchronize do
1262
+ @isolation_level
1263
+ end
1264
+ end
1265
+
1266
+ # Starts a transaction or "nested transaction" (implemented by using save points), if already one is in progress. The arguments to this function can be an IsolationLevel constant, a symbol representing an IsolationLevel, the special symbol :unless_open or any number of BaseRecord objects, which are copied with BaseRecord#dup and restored with BaseRecord#replace, in case the transaction fails. If :unless_open is specified, and a transaction is already open, this method does nothing, except calling the given block. As partitial rollbacks on errors won't happen in this case, it's not recommended to use the :unless_open parameter, unless you know what you are doing. IsolationLevel's are only regarded, if there is no transaction open yet.
1267
+ def transaction(*arguments)
1268
+ isolation_level = nil
1269
+ records = []
1270
+ unless_open_mode = false
1271
+ arguments.flatten.each do |argument|
1272
+ if argument.kind_of? FlexiRecord::IsolationLevel
1273
+ isolation_level_argument = argument
1274
+ elsif argument.respond_to? :to_sym
1275
+ isolation_level_argument = FlexiRecord::IsolationLevel.by_symbol(argument)
1276
+ end
1277
+ if argument.nil?
1278
+ # nothing
1279
+ elsif argument == :unless_open
1280
+ unless_open_mode = true
1281
+ elsif isolation_level_argument
1282
+ unless isolation_level.nil?
1283
+ raise ArgumentError, "Multiple isolation levels given."
1284
+ end
1285
+ isolation_level = isolation_level_argument
1286
+ elsif argument.kind_of? Symbol
1287
+ raise ArgumentError, "Unknown symbol #{argument.inspect} given as argument to transaction method."
1288
+ else
1289
+ records << argument
1290
+ end
1291
+ end
1292
+ if unless_open_mode and not records.empty?
1293
+ raise ArgumentError, "No records may be specified, if a transaction is started 'unless_open'."
1294
+ end
1295
+ synchronize do
1296
+ if unless_open_mode and transaction?
1297
+ return yield
1298
+ end
1299
+ backup = records.collect { |record| record.dup }
1300
+ old_stacklevel = @transaction_stacklevel
1301
+ success = true
1302
+ begin
1303
+ if @transaction_stacklevel == 0
1304
+ @isolation_level = isolation_level
1305
+ if isolation_level
1306
+ execute("BEGIN TRANSACTION ISOLATION LEVEL #{isolation_level}")
1307
+ else
1308
+ execute('BEGIN TRANSACTION')
1309
+ end
1310
+ else
1311
+ execute('SAVEPOINT "FlexiRecord_' << @transaction_stacklevel.to_s << '"')
1312
+ end
1313
+ @transaction_stacklevel += 1
1314
+ return yield
1315
+ rescue StandardError
1316
+ success = false
1317
+ raise $!
1318
+ ensure
1319
+ @transaction_stacklevel = old_stacklevel
1320
+ if success
1321
+ if old_stacklevel == 0
1322
+ execute('COMMIT TRANSACTION')
1323
+ else
1324
+ execute('RELEASE SAVEPOINT "FlexiRecord_' << @transaction_stacklevel.to_s << '"')
1325
+ end
1326
+ else
1327
+ if old_stacklevel == 0
1328
+ @isolation_level = 0
1329
+ execute('ROLLBACK TRANSACTION')
1330
+ else
1331
+ execute('ROLLBACK TO SAVEPOINT "FlexiRecord_' << @transaction_stacklevel.to_s << '"')
1332
+ execute('RELEASE SAVEPOINT "FlexiRecord_' << @transaction_stacklevel.to_s << '"')
1333
+ end
1334
+ backup.each_index do |record_index|
1335
+ records[record_index].replace(backup[record_index])
1336
+ end
1337
+ end
1338
+ end
1339
+ end
1340
+ end
1341
+
1342
+ # Closes a transaction, which can't not be used anymore.
1343
+ def close
1344
+ @backend_connection.close
1345
+ @backend_connection = nil
1346
+ nil
1347
+ end
1348
+
1349
+ end # end of class Connection
1350
+
1351
+
1352
+ # A pool of database connections to be used exclusively by one thread at a time.
1353
+
1354
+ class ConnectionPool < ThreadResourcePool
1355
+
1356
+ # Creates a new ConnectionPool which automatically generates and caches Connection objects with the given options.
1357
+ def initialize(connection_options)
1358
+ @connection_options = connection_options.dup
1359
+ pool_size = @connection_options.delete(:pool_size)
1360
+ super(pool_size || 10)
1361
+ end
1362
+
1363
+ # Implementation of ThreadResourcePool#generate_resource.
1364
+ def generate_resource
1365
+ FlexiRecord::Connection.new(@connection_options)
1366
+ end
1367
+
1368
+ # Implementation of ThreadResourcePool#reset_resource.
1369
+ def reset_resource(connection)
1370
+ true
1371
+ end
1372
+
1373
+ # Implementation of ThreadResourcePool#destroy_resource.
1374
+ def destroy_resource(connection)
1375
+ connection.close
1376
+ end
1377
+
1378
+ # Passes a Connection object to the given block, which then can be used by the current thread.
1379
+
1380
+ def use_connection(*args, &block)
1381
+ use_resource(*args, &block)
1382
+ end
1383
+
1384
+ # Wrapper for Connection#transaction for the Connection of the current thread.
1385
+ def transaction(*arguments)
1386
+ use_connection do |connection|
1387
+ connection.transaction(*arguments) do
1388
+ return yield
1389
+ end
1390
+ end
1391
+ end
1392
+
1393
+ end # end of class ConnectionPool
1394
+
1395
+
1396
+ # MixIn for BaseRecord's (or objects of sub-classes) which are sorted by a 'position' field.
1397
+
1398
+ module ListRecord
1399
+
1400
+ def save
1401
+ self.class.transaction(self, :read_committed) do
1402
+ self.class.db_execute "LOCK TABLE #{self.class.table} IN SHARE ROW EXCLUSIVE MODE"
1403
+ if self.position == :first
1404
+ db_result = self.class.db_query1(
1405
+ 'SELECT COALESCE("position" - 1, 0) AS "result" ' <<
1406
+ 'FROM ' << self.class.table << ' WHERE "position" NOTNULL ORDER BY "position" ASC)')
1407
+ elsif self.position == :last
1408
+ db_result = self.class.db_query1(
1409
+ 'SELECT COALESCE("position" + 1, 0) AS "result" ' <<
1410
+ 'FROM ' << self.class.table << ' WHERE "position" NOTNULL ORDER BY "position" DESC')
1411
+ end
1412
+ if db_result
1413
+ self.position = db_result.result
1414
+ else
1415
+ self.position = 0
1416
+ end
1417
+ return super
1418
+ end
1419
+ end
1420
+
1421
+ end # end of mix-in ListRecord
1422
+
1423
+
1424
+ end # end of module FlexiRecord
1425
+