flexirecord 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +23 -0
- data/flexirecord-demo.sql +33 -0
- data/lib/flexirecord-demo.rb +145 -0
- data/lib/flexirecord.rb +1425 -0
- data/lib/thread_resource_pool.rb +118 -0
- metadata +55 -0
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
|
data/lib/flexirecord.rb
ADDED
@@ -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
|
+
|