webink 1.1.4
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/rfcgi +112 -0
- data/bin/webink_database +50 -0
- data/lib/webink.rb +4 -0
- data/lib/webink/beauty.rb +80 -0
- data/lib/webink/controller.rb +285 -0
- data/lib/webink/database.rb +425 -0
- data/lib/webink/model.rb +311 -0
- metadata +107 -0
@@ -0,0 +1,425 @@
|
|
1
|
+
module Ink
|
2
|
+
|
3
|
+
# = Database class
|
4
|
+
#
|
5
|
+
# == Config
|
6
|
+
#
|
7
|
+
# Currently there are two types of databases supported, MySQL and
|
8
|
+
# SQLite3. Either way, you need to specify them in a config-file
|
9
|
+
# that is located inside the web project folder.
|
10
|
+
#
|
11
|
+
# Sample config for MySQL:
|
12
|
+
# config = {
|
13
|
+
# "escape_post_data" => false,
|
14
|
+
# "production" => true,
|
15
|
+
# "db_type" => "mysql",
|
16
|
+
# "db_user" => "yourusername",
|
17
|
+
# "db_pass" => "yourpassword",
|
18
|
+
# "db_database" => "yourdatabase",
|
19
|
+
# "db_server" => "localhost",
|
20
|
+
# }
|
21
|
+
#
|
22
|
+
# Sample config for SQLite3:
|
23
|
+
# config = {
|
24
|
+
# "escape_post_data" => false,
|
25
|
+
# "production" => true,
|
26
|
+
# "db_type" => "sqlite3",
|
27
|
+
# "db_server" => "/full/path/to/database.sqlite",
|
28
|
+
# }
|
29
|
+
#
|
30
|
+
# == Usage
|
31
|
+
#
|
32
|
+
# Create an Ink::Database instance with the self.create class method.
|
33
|
+
# Now it can be accessed via the public class variable 'database'.
|
34
|
+
# Once that is done, can use it to execute various SQL statements
|
35
|
+
# to gather data.
|
36
|
+
#
|
37
|
+
# Ink::Database.database.query "SELECT * FROM x;"
|
38
|
+
#
|
39
|
+
# This is the most basic query, it returns an Array of results,
|
40
|
+
# and each element contains a Hash of column_name => column_entry.
|
41
|
+
#
|
42
|
+
# Ink::Database.database.find "apples", "WHERE id < 10 GROUP BY color"
|
43
|
+
# => self.query("SELECT * FROM apples WHERE id < 10 GROUP BY color;")
|
44
|
+
#
|
45
|
+
# This is different from the query method, because it returns an Array
|
46
|
+
# of Objects, created by the information stored in the database. So this
|
47
|
+
# find() will return you a set of Apple-instances.
|
48
|
+
#
|
49
|
+
# Ink::Database.database.find_union "apple", 5, "tree", ""
|
50
|
+
#
|
51
|
+
# find_union allows you to retrieve data through a many_many reference.
|
52
|
+
# When you define a many_many relationship, a helper-table is created
|
53
|
+
# inside the database, which is apple_tree, in this case. This statement
|
54
|
+
# will fetch an Array of all Trees, that connect to the Apple with primary
|
55
|
+
# key 5. Notice that the relationship database apple_tree is put together
|
56
|
+
# by the alphabetically first, and then second classname. The last quotes
|
57
|
+
# allow additional query informations to be passed along (like group by)
|
58
|
+
#
|
59
|
+
# Ink::Database.database.find_reference "tree", 1, "apple", ""
|
60
|
+
#
|
61
|
+
# find_reference is similar to find_union, only that it handles all
|
62
|
+
# other relationships. This statement above requires one Tree to have many
|
63
|
+
# Apples, so it will return an Array of Apples, all those that belong to
|
64
|
+
# the Tree with primary key 1
|
65
|
+
#
|
66
|
+
# Please close the dbinstance once you are done. This is automatically
|
67
|
+
# inserted in the init.rb of a project.
|
68
|
+
#
|
69
|
+
#
|
70
|
+
#
|
71
|
+
class Database
|
72
|
+
private_class_method :new
|
73
|
+
@@database = nil
|
74
|
+
|
75
|
+
# Private Constructor
|
76
|
+
#
|
77
|
+
# Uses the config parameter to create a database
|
78
|
+
# connection, and will throw an error, if that is not
|
79
|
+
# possible.
|
80
|
+
# [param config:] Hash of config parameters
|
81
|
+
def initialize(config)
|
82
|
+
@type = config["db_type"]
|
83
|
+
if @type == "mysql"
|
84
|
+
@db = Mysql.real_connect(config["db_server"],config["db_user"],config["db_pass"],config["db_database"])
|
85
|
+
@db.reconnect = true
|
86
|
+
elsif @type == "sqlite3"
|
87
|
+
@db = SQLite3::Database.new(config["db_server"])
|
88
|
+
else
|
89
|
+
raise ArgumentError.new("Database undefined.")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Class method
|
94
|
+
#
|
95
|
+
# Instanciates a new Database if none is found
|
96
|
+
# [param config:] Hash of config parameters
|
97
|
+
def self.create(config)
|
98
|
+
@@database = new(config) if not @@database
|
99
|
+
end
|
100
|
+
|
101
|
+
# Class method
|
102
|
+
#
|
103
|
+
# Removes an instanciated Database
|
104
|
+
def self.drop
|
105
|
+
@@database = nil if @@database
|
106
|
+
end
|
107
|
+
|
108
|
+
# Class method
|
109
|
+
#
|
110
|
+
# Returns the Database instance or raises a Runtime Error
|
111
|
+
# [returns:] Database instance
|
112
|
+
def self.database
|
113
|
+
(@@database) ? @@database : (raise RuntimeError.new("No Database found. Create one first"))
|
114
|
+
end
|
115
|
+
|
116
|
+
# Instance method
|
117
|
+
#
|
118
|
+
# This will retrieve all tables nested into
|
119
|
+
# the connected database.
|
120
|
+
# [returns:] Array of tables
|
121
|
+
def tables
|
122
|
+
result = Array.new
|
123
|
+
if @type == "mysql"
|
124
|
+
re = @db.query "show tables;"
|
125
|
+
re.each do |row|
|
126
|
+
row.each do |t|
|
127
|
+
result.push t
|
128
|
+
end
|
129
|
+
end
|
130
|
+
elsif @type == "sqlite3"
|
131
|
+
re = @db.query "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"
|
132
|
+
re.each do |row|
|
133
|
+
row.each do |t|
|
134
|
+
result.push t
|
135
|
+
end
|
136
|
+
end
|
137
|
+
re.close
|
138
|
+
end
|
139
|
+
result
|
140
|
+
end
|
141
|
+
|
142
|
+
# Instance method
|
143
|
+
#
|
144
|
+
# Send an SQL query string to the database
|
145
|
+
# and retrieve a result set
|
146
|
+
# [param query:] SQL query string
|
147
|
+
# [returns:] Array of Hashes of column_name => column_entry
|
148
|
+
def query(query)
|
149
|
+
result = Array.new
|
150
|
+
if @type == "mysql"
|
151
|
+
re = @db.method("query").call query
|
152
|
+
if re
|
153
|
+
re.each_hash do |row|
|
154
|
+
result.push Hash.new
|
155
|
+
row.each do |k,v|
|
156
|
+
v = $&.to_i if v =~ /^[0-9]+$/
|
157
|
+
result[result.length-1][k] = v
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
elsif @type == "sqlite3"
|
162
|
+
re = @db.method("query").call query
|
163
|
+
re.each do |row|
|
164
|
+
result.push Hash.new
|
165
|
+
for i in 0...re.columns.length
|
166
|
+
row[i] = $&.to_i if row[i] =~ /^[0-9]+$/
|
167
|
+
result[result.length-1][re.columns[i]] = row[i]
|
168
|
+
end
|
169
|
+
end
|
170
|
+
re.close if not re.closed?
|
171
|
+
end
|
172
|
+
result
|
173
|
+
end
|
174
|
+
|
175
|
+
# Instance method
|
176
|
+
#
|
177
|
+
# Closes the database connection, there is no way
|
178
|
+
# to reopen without creating a new Ink::Database instance
|
179
|
+
def close
|
180
|
+
if @type == "sqlite3" and not @db.closed?
|
181
|
+
begin
|
182
|
+
@db.close
|
183
|
+
rescue SQLite3::BusyException
|
184
|
+
end
|
185
|
+
elsif @type == "mysql"
|
186
|
+
@db.close
|
187
|
+
end
|
188
|
+
self.class.drop
|
189
|
+
end
|
190
|
+
|
191
|
+
# Instance method
|
192
|
+
#
|
193
|
+
# Attempts to fetch the last inserted primary key
|
194
|
+
# [returns:] primary key or nil
|
195
|
+
def last_inserted_pk
|
196
|
+
string = ""
|
197
|
+
if @type == "mysql"
|
198
|
+
string = "LAST_INSERT_ID()"
|
199
|
+
elsif @type == "sqlite3"
|
200
|
+
string = "last_insert_rowid()"
|
201
|
+
end
|
202
|
+
response = self.query "SELECT #{string} as id"
|
203
|
+
return (response.length > 0) ? response[0]["id"] : nil
|
204
|
+
end
|
205
|
+
|
206
|
+
# Instance method
|
207
|
+
#
|
208
|
+
# Creates the SQL syntax for the chosen database type
|
209
|
+
# to define a primary key, autoincrementing field
|
210
|
+
# [returns:] SQL syntax for a primary key field
|
211
|
+
def primary_key_autoincrement(pk="id")
|
212
|
+
result = Array.new
|
213
|
+
if @type == "mysql"
|
214
|
+
result = ["`#{pk}`", "INTEGER", "PRIMARY KEY", "AUTO_INCREMENT"]
|
215
|
+
elsif @type == "sqlite3"
|
216
|
+
result = ["`#{pk}`", "INTEGER", "PRIMARY KEY", "ASC"]
|
217
|
+
end
|
218
|
+
result
|
219
|
+
end
|
220
|
+
|
221
|
+
# Instance method
|
222
|
+
#
|
223
|
+
# Delete something from the database.
|
224
|
+
# [param class_name:] Defines the table name
|
225
|
+
# [param params:] Additional SQL syntax like WHERE conditions (optional)
|
226
|
+
def remove(class_name, params="")
|
227
|
+
table_name = Ink::Model.str_to_tablename(class_name)
|
228
|
+
return if not table_name
|
229
|
+
self.query("DELETE FROM #{table_name} #{params};")
|
230
|
+
end
|
231
|
+
|
232
|
+
# Instance method
|
233
|
+
#
|
234
|
+
# Retrieve class instances, that are loaded with the database result set.
|
235
|
+
# [param class_name:] Defines the table name and resulting Instance classnames
|
236
|
+
# [param params:] Additional SQL syntax like WHERE conditions (optional)
|
237
|
+
# [returns:] Array of class_name instances from the SQL result set
|
238
|
+
def find(class_name, params="")
|
239
|
+
result = Array.new
|
240
|
+
table_name = Ink::Model.str_to_tablename(class_name)
|
241
|
+
return result if not table_name
|
242
|
+
|
243
|
+
re = self.query("SELECT * FROM #{table_name} #{params};")
|
244
|
+
re.each do |entry|
|
245
|
+
instance = Ink::Model.classname(class_name).new entry
|
246
|
+
result.push instance
|
247
|
+
end
|
248
|
+
result
|
249
|
+
end
|
250
|
+
|
251
|
+
# Instance method
|
252
|
+
#
|
253
|
+
# Retrieve class2 instances, that are related to the class1 instance with
|
254
|
+
# primary key class1_id. This is done via an additional relationship table.
|
255
|
+
# Only relevant for many_many relationships.
|
256
|
+
# [param class1:] Reference classname
|
257
|
+
# [param class1_id:] Primary key value of the reference classname
|
258
|
+
# [param class2:] Match classname
|
259
|
+
# [param params:] Additional SQL syntax like GROUP BY (optional)
|
260
|
+
# [returns:] Array of class2 instances from the SQL result set
|
261
|
+
def find_union(class1, class1_id, class2, params="")
|
262
|
+
result = Array.new
|
263
|
+
relationship = nil
|
264
|
+
Ink::Model.classname(class1).foreign.each do |k,v|
|
265
|
+
relationship = v if k.downcase == class2.downcase
|
266
|
+
end
|
267
|
+
return result if relationship != "many_many"
|
268
|
+
fk1 = Ink::Model.classname(class1).foreign_key[0]
|
269
|
+
pk2 = Ink::Model.classname(class2).primary_key[0]
|
270
|
+
fk2 = Ink::Model.classname(class2).foreign_key[0]
|
271
|
+
tablename1 = Ink::Model.str_to_tablename(class1)
|
272
|
+
tablename2 = Ink::Model.str_to_tablename(class2)
|
273
|
+
union_class = ((class1.downcase <=> class2.downcase) < 0) ? "#{tablename1}_#{tablename2}" : "#{tablename2}_#{tablename1}"
|
274
|
+
re = self.query("SELECT #{tablename2}.* FROM #{union_class}, #{tablename2} WHERE #{union_class}.#{fk1} = #{class1_id} AND #{union_class}.#{fk2} = #{tablename2}.#{pk2} #{params};")
|
275
|
+
re.each do |entry|
|
276
|
+
instance = Ink::Model.classname(tablename2).new entry
|
277
|
+
result.push instance
|
278
|
+
end
|
279
|
+
result
|
280
|
+
end
|
281
|
+
|
282
|
+
# Instance method
|
283
|
+
#
|
284
|
+
# Retrieve class2 instances, that are related to the class1 instance with
|
285
|
+
# primary key class1_id. Not relevant for many_many relationships
|
286
|
+
# [param class1:] Reference classname
|
287
|
+
# [param class1_id:] Primary key value of the reference classname
|
288
|
+
# [param class2:] Match classname
|
289
|
+
# [param params:] Additional SQL syntax like GROUP BY (optional)
|
290
|
+
# [returns:] Array of class2 instances from the SQL result set
|
291
|
+
def find_references(class1, class1_id, class2, params="")
|
292
|
+
result = Array.new
|
293
|
+
relationship = nil
|
294
|
+
Ink::Model.classname(class1).foreign.each do |k,v|
|
295
|
+
relationship = v if k.downcase == class2.downcase
|
296
|
+
end
|
297
|
+
return result if relationship == "many_many"
|
298
|
+
re = Array.new
|
299
|
+
fk1 = Ink::Model.classname(class1).foreign_key[0]
|
300
|
+
if ((class1.downcase <=> class2.downcase) < 0 and relationship == "one_one") or relationship == "one_many"
|
301
|
+
re = self.query "SELECT * FROM #{Ink::Model.str_to_tablename(class2)} WHERE #{Ink::Model.classname(class2).primary_key[0]}=(SELECT #{Ink::Model.classname(class2).foreign_key[0]} FROM #{Ink::Model.str_to_tablename(class1)} WHERE #{Ink::Model.classname(class1).primary_key[0]}=#{class1_id});"
|
302
|
+
else
|
303
|
+
re = self.query "SELECT * FROM #{Ink::Model.str_to_tablename(class2)} WHERE #{fk1} = #{class1_id} #{params};"
|
304
|
+
end
|
305
|
+
|
306
|
+
re.each do |entry|
|
307
|
+
instance = Ink::Model.classname(class2).new entry
|
308
|
+
result.push instance
|
309
|
+
end
|
310
|
+
result
|
311
|
+
end
|
312
|
+
|
313
|
+
# Instance method
|
314
|
+
#
|
315
|
+
# This method attempts to remove all existing relationship data
|
316
|
+
# of instance with link of type: type. For one_one relationships
|
317
|
+
# this works only one way, requiring a second call later on before
|
318
|
+
# setting a new value.
|
319
|
+
# [param instance:] Instance of a class that refers to an existing database entry
|
320
|
+
# [param link:] the related class (not a String, but class reference)
|
321
|
+
# [param type:] relationship type
|
322
|
+
def delete_all_links(instance, link, type)
|
323
|
+
if type == "one_one"
|
324
|
+
firstclass = ((instance.class.name.downcase <=> link.name.downcase) < 0) ? instance.class : link
|
325
|
+
secondclass = ((instance.class.name.downcase <=> link.name.downcase) < 0) ? link : instance.class
|
326
|
+
key = ((instance.class.name.downcase <=> link.name.downcase) < 0) ? instance.class.primary_key[0] : instance.class.foreign_key[0]
|
327
|
+
value = instance.method(instance.class.primary_key[0]).call
|
328
|
+
@db.query "UPDATE #{Ink::Model.str_to_tablename(firstclass.name)} SET #{secondclass.foreign_key[0]}=NULL WHERE #{key}=#{value};"
|
329
|
+
elsif type == "one_many" or type == "many_one"
|
330
|
+
firstclass = (type == "one_many") ? instance.class : link
|
331
|
+
secondclass = (type == "one_many") ? link : instance.class
|
332
|
+
key = (type == "one_many") ? instance.class.primary_key[0] : instance.class.foreign_key[0]
|
333
|
+
value = instance.method(instance.class.primary_key[0]).call
|
334
|
+
@db.query "UPDATE #{Ink::Model.str_to_tablename(firstclass.name)} SET #{secondclass.foreign_key[0]}=NULL WHERE #{key}=#{value};"
|
335
|
+
elsif type == "many_many"
|
336
|
+
tablename1 = Ink::Model.str_to_tablename(instance.class.name)
|
337
|
+
tablename2 = Ink::Model.str_to_tablename(link.name)
|
338
|
+
union_class = ((instance.class.name.downcase <=> link.name.downcase) < 0) ? "#{tablename1}_#{tablename2}" : "#{tablename2}_#{tablename1}"
|
339
|
+
value = instance.method(instance.class.primary_key[0]).call
|
340
|
+
@db.query "DELETE FROM #{union_class} WHERE #{instance.class.foreign_key[0]}=#{value};"
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
# Instance method
|
345
|
+
#
|
346
|
+
# Attempt to create links of instance to the data inside value.
|
347
|
+
# link is the class of the related data, and type refers to the
|
348
|
+
# relationship type of the two. When one tries to insert an array
|
349
|
+
# for a x_one relationship, the last entry will be set.
|
350
|
+
# [param instance:] Instance of a class that refers to an existing database entry
|
351
|
+
# [param link:] the related class (not a String, but class reference)
|
352
|
+
# [param type:] relationship type
|
353
|
+
# [param value:] relationship data that was set, either a primary key value, or an instance, or an array of both
|
354
|
+
def create_all_links(instance, link, type, value)
|
355
|
+
to_add = Array.new
|
356
|
+
if value.is_a? Array
|
357
|
+
value.each do |v|
|
358
|
+
if v.instance_of? link
|
359
|
+
to_add.push(v.method(link.primary_key[0]).call)
|
360
|
+
else
|
361
|
+
to_add.push v
|
362
|
+
end
|
363
|
+
end
|
364
|
+
elsif value.instance_of? link
|
365
|
+
to_add.push(value.method(link.primary_key[0]).call)
|
366
|
+
else
|
367
|
+
to_add.push value
|
368
|
+
end
|
369
|
+
to_add.each do |fk|
|
370
|
+
self.create_link instance, link, type, fk
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
# Instance method
|
375
|
+
#
|
376
|
+
# Creates a link between instance and a link with primary fk.
|
377
|
+
# The relationship between the two is defined by type. one_one
|
378
|
+
# relationships are placing an additional call to delete_all_links
|
379
|
+
# that will remove conflicts.
|
380
|
+
# [param instance:] Instance of a class that refers to an existing database entry
|
381
|
+
# [param link:] the related class (not a String, but class reference)
|
382
|
+
# [param type:] relationship type
|
383
|
+
# [param value:] primary key of the relationship, that is to be created
|
384
|
+
def create_link(instance, link, type, fk)
|
385
|
+
if type == "one_one"
|
386
|
+
if (instance.class.name.downcase <=> link.name.downcase) < 0
|
387
|
+
re = self.find(link.name, "WHERE #{link.primary_key[0]}=#{fk};")[0]
|
388
|
+
self.delete_all_links re, instance.class, type
|
389
|
+
end
|
390
|
+
firstclass = ((instance.class.name.downcase <=> link.name.downcase) < 0) ? instance.class : link
|
391
|
+
secondclass = ((instance.class.name.downcase <=> link.name.downcase) < 0) ? link : instance.class
|
392
|
+
key = ((instance.class.name.downcase <=> link.name.downcase) < 0) ? instance.class.primary_key[0] : link.primary_key[0]
|
393
|
+
value = instance.method(instance.class.primary_key[0]).call
|
394
|
+
fk_set = ((instance.class.name.downcase <=> link.name.downcase) < 0) ? fk : value
|
395
|
+
value_set = ((instance.class.name.downcase <=> link.name.downcase) < 0) ? value : fk
|
396
|
+
@db.query "UPDATE #{Ink::Model.str_to_tablename(firstclass.name)} SET #{secondclass.foreign_key[0]}=#{fk} WHERE #{key}=#{value};"
|
397
|
+
elsif type == "one_many" or type == "many_one"
|
398
|
+
firstclass = (type == "one_many") ? instance.class : link
|
399
|
+
secondclass = (type == "one_many") ? link : instance.class
|
400
|
+
key = (type == "one_many") ? instance.class.primary_key[0] : link.primary_key[0]
|
401
|
+
value = instance.method(instance.class.primary_key[0]).call
|
402
|
+
fk_set = (type == "one_many") ? fk : value
|
403
|
+
value_set = (type == "one_many") ? value : fk
|
404
|
+
@db.query "UPDATE #{Ink::Model.str_to_tablename(firstclass.name)} SET #{secondclass.foreign_key[0]}=#{fk_set} WHERE #{key}=#{value_set};"
|
405
|
+
elsif type == "many_many"
|
406
|
+
tablename1 = Ink::Model.str_to_tablename(instance.class.name)
|
407
|
+
tablename2 = Ink::Model.str_to_tablename(link.name)
|
408
|
+
union_class = ((instance.class.name.downcase <=> link.name.downcase) < 0) ? "#{tablename1}_#{tablename2}" : "#{tablename2}_#{tablename1}"
|
409
|
+
value = instance.method(instance.class.primary_key[0]).call
|
410
|
+
@db.query "INSERT INTO #{union_class} (#{instance.class.foreign_key[0]}, #{link.foreign_key[0]}) VALUES (#{value}, #{fk});"
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
# Class method
|
415
|
+
#
|
416
|
+
# Formats a Time object according to the SQL TimeDate standard
|
417
|
+
# [param date:] Time object
|
418
|
+
# [returns:] Formatted string
|
419
|
+
def self.format_date(date)
|
420
|
+
(date.instance_of? Time) ? date.strftime("%Y-%m-%d %H:%M:%S") : ""
|
421
|
+
end
|
422
|
+
|
423
|
+
end
|
424
|
+
|
425
|
+
end
|
data/lib/webink/model.rb
ADDED
@@ -0,0 +1,311 @@
|
|
1
|
+
module Ink
|
2
|
+
|
3
|
+
# = Model class
|
4
|
+
#
|
5
|
+
# == Usage
|
6
|
+
#
|
7
|
+
# Models are usually derived from. So let's assume there is a
|
8
|
+
# class called Apple < Ink::Model
|
9
|
+
#
|
10
|
+
# apple = Apple.new {:color => "red", :diameter => 4}
|
11
|
+
#
|
12
|
+
# The constructor checks, if there are class methods 'fields'
|
13
|
+
# and 'foreign' defined. If that check is positive, it will
|
14
|
+
# match the parameter Hash to the fields, that are set for
|
15
|
+
# the database, and thow an exception if fields is lacking
|
16
|
+
# an entry (excluded the primary key). The other case just
|
17
|
+
# creates an Apple with the Hash as instance variables.
|
18
|
+
#
|
19
|
+
# puts apple.color
|
20
|
+
#
|
21
|
+
# This prints "red" to the stdout, since getter and setter
|
22
|
+
# methods are automatically added for either the Hash, or
|
23
|
+
# the fields and foreign keys.
|
24
|
+
#
|
25
|
+
# apple.tree = nil
|
26
|
+
# apple.save
|
27
|
+
#
|
28
|
+
# You can save your apple by using the save method. New instances
|
29
|
+
# will create a new row in the database, and update its primary
|
30
|
+
# key. Old instances just update the fields. Relationships, like
|
31
|
+
# in the sample below a tree, is set to nil by default, and therefore
|
32
|
+
# the save method will not touch relationships.
|
33
|
+
#
|
34
|
+
# treeinstance.apple = [1,2,myapple]
|
35
|
+
# treeinstance.save
|
36
|
+
#
|
37
|
+
# To insert relationship data, you can provide them by array, value
|
38
|
+
# or reference, so setting treeinstance.apple to 1 is allowed, also
|
39
|
+
# to myapple, or an array or a combination. An empty array [] will
|
40
|
+
# remove all references. This works both ways, just consider the
|
41
|
+
# relationship type, as an apple cannot have more than one tree.
|
42
|
+
#
|
43
|
+
# treeinstance.delete
|
44
|
+
#
|
45
|
+
# The model provides a convenience method for deletion. It removes all
|
46
|
+
# references from relationships, but does not remove the relationships
|
47
|
+
# themselves, so you must fetch all related data, and delete them by
|
48
|
+
# 'hand' if you will.
|
49
|
+
#
|
50
|
+
#
|
51
|
+
# = Fields and foreign sample config
|
52
|
+
#
|
53
|
+
# class Apple < Ink::Model
|
54
|
+
# def self.fields
|
55
|
+
# fields = {
|
56
|
+
# :id => "PRIMARY KEY"
|
57
|
+
# :color => [ "VARCHAR", "NOT NULL" ],
|
58
|
+
# :diameter => [ "NUMERIC", "NOT NULL" ]
|
59
|
+
# }
|
60
|
+
# fields
|
61
|
+
# end
|
62
|
+
# def self.foreign
|
63
|
+
# foreign = {
|
64
|
+
# "Tree" => "one_many"
|
65
|
+
# }
|
66
|
+
# foreign
|
67
|
+
# end
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# Let's look at this construct.
|
71
|
+
# The constructor is inherited from Ink::Model, so are its
|
72
|
+
# methods. 'fields' defines a Hash of Arrays, that will
|
73
|
+
# create the Database table for us.
|
74
|
+
# 'foreign' handles the contraints to other classes, here
|
75
|
+
# it reads: one "Tree" has many Apples, other constructs
|
76
|
+
# could be: [one "Tree" has one Apple, many "Tree"s have
|
77
|
+
# many Apples, many "Tree"s have one Apple] => [one_one,
|
78
|
+
# many_many, many_one]
|
79
|
+
# Obviously the Tree class requires a foreign with "Apple"
|
80
|
+
# mapped to "many_one" to match this schema.
|
81
|
+
#
|
82
|
+
#
|
83
|
+
#
|
84
|
+
class Model
|
85
|
+
|
86
|
+
# Constructor
|
87
|
+
#
|
88
|
+
# Keys from the data parameter will be converted into
|
89
|
+
# instance variables with getters and setters in place.
|
90
|
+
# The primary key has no setter, but adds a getter called
|
91
|
+
# pk for convenience.
|
92
|
+
# [param data:] Hash of String => Objects
|
93
|
+
def initialize(data)
|
94
|
+
if self.class.respond_to? :fields
|
95
|
+
self.class.fields.each do |k,v|
|
96
|
+
raise NameError.new("Model cannot use #{k} as field, it already exists") if self.class.respond_to? k or k.to_s.downcase == "pk"
|
97
|
+
raise LoadError.new("Model cannot be loaded, argument missing: #{k}") if not data[k.to_s] and self.class.primary_key[0] != k
|
98
|
+
entry = nil
|
99
|
+
if data[k.to_s].is_a? String
|
100
|
+
entry = data[k.to_s].gsub(/'/, ''')
|
101
|
+
elsif data[k.to_s].is_a? Numeric
|
102
|
+
entry = data[k.to_s]
|
103
|
+
else
|
104
|
+
entry = "\'#{data[k.to_s]}\'"
|
105
|
+
end
|
106
|
+
instance_variable_set("@#{k}", entry)
|
107
|
+
|
108
|
+
self.class.send(:define_method, k) do
|
109
|
+
instance_variable_get "@#{k}"
|
110
|
+
end
|
111
|
+
if self.class.primary_key[0] != k
|
112
|
+
self.class.send(:define_method, "#{k}=") do |val|
|
113
|
+
if val.is_a? String
|
114
|
+
val = val.gsub(/'/, ''')
|
115
|
+
elsif val.is_a? Numeric
|
116
|
+
val = val
|
117
|
+
else
|
118
|
+
val = "\'#{val}\'"
|
119
|
+
end
|
120
|
+
instance_variable_set "@#{k}", val
|
121
|
+
end
|
122
|
+
else
|
123
|
+
self.class.send(:define_method, "pk") do
|
124
|
+
instance_variable_get "@#{k.to_s.downcase}"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
if self.class.respond_to? :foreign
|
129
|
+
self.class.foreign.each do |k,v|
|
130
|
+
raise NameError.new("Model cannot use #{k} as foreign, it already exists") if self.class.respond_to? k.to_sym or k.downcase == "pk"
|
131
|
+
instance_variable_set "@#{self.class.str_to_tablename(k)}", nil
|
132
|
+
self.class.send(:define_method, k.downcase) do
|
133
|
+
instance_variable_get "@#{k.downcase}"
|
134
|
+
end
|
135
|
+
self.class.send(:define_method, "#{k.downcase}=") do |val|
|
136
|
+
instance_variable_set "@#{k.downcase}", val
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
else
|
141
|
+
data.each do |k,v|
|
142
|
+
instance_variable_set "@#{k}", (v.is_a?(Numeric)) ? v : "\'#{v}\'"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Instance method
|
148
|
+
#
|
149
|
+
# Save the instance to the database. Set all foreign sets to
|
150
|
+
# nil if you do not want to change them. Old references are
|
151
|
+
# automatically removed.
|
152
|
+
def save
|
153
|
+
raise NotImplementedError.new("Cannot save to Database without field definitions") if not self.class.respond_to? :fields
|
154
|
+
string = Array.new
|
155
|
+
keystring = Array.new
|
156
|
+
valuestring = Array.new
|
157
|
+
fields = self.class.fields
|
158
|
+
pkvalue = nil
|
159
|
+
for i in 0...fields.keys.length
|
160
|
+
k = fields.keys[i]
|
161
|
+
value = instance_variable_get "@#{k}"
|
162
|
+
value = "NULL" if not value
|
163
|
+
if k != self.class.primary_key[0]
|
164
|
+
string.push "`#{k}`=#{(value.is_a?(Numeric)) ? value : "\'#{value}\'"}"
|
165
|
+
keystring.push "`#{k}`"
|
166
|
+
valuestring.push "#{(value.is_a?(Numeric)) ? value : "\'#{value}\'"}"
|
167
|
+
else
|
168
|
+
pkvalue = "WHERE `#{self.class.primary_key[0]}`=#{(value.is_a?(Numeric)) ? value : "\'#{value}\'"}"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
if pkvalue
|
172
|
+
response = Ink::Database.database.find self.class.name, pkvalue
|
173
|
+
if response.length == 1
|
174
|
+
Ink::Database.database.query "UPDATE #{Ink::Model.str_to_tablename(self.class.name)} SET #{string * ","} #{pkvalue}"
|
175
|
+
elsif response.length == 0
|
176
|
+
Ink::Database.database.query "INSERT INTO #{Ink::Model.str_to_tablename(self.class.name)} (#{keystring * ","}) VALUES (#{valuestring * ","});"
|
177
|
+
pk = Ink::Database.database.last_inserted_pk
|
178
|
+
instance_variable_set "@#{self.class.primary_key[0]}", pk.is_a?(Numeric) ? pk : "\'#{pk}\'" if pk
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
if self.class.respond_to? :foreign
|
183
|
+
self.class.foreign.each do |k,v|
|
184
|
+
value = instance_variable_get "@#{self.class.str_to_tablename(k)}"
|
185
|
+
if value
|
186
|
+
Ink::Database.database.delete_all_links self, Ink::Model.classname(k), v
|
187
|
+
Ink::Database.database.create_all_links self, Ink::Model.classname(k), v, value
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Instance method
|
194
|
+
#
|
195
|
+
# Deletes the data from the database, essentially making the instance
|
196
|
+
# obsolete. Disregard from using the instance anymore.
|
197
|
+
# All links between models will be removed also.
|
198
|
+
def delete
|
199
|
+
raise NotImplementedError.new("Cannot delete from Database without field definitions") if not self.class.respond_to? :fields
|
200
|
+
if self.class.respond_to? :foreign
|
201
|
+
self.class.foreign.each do |k,v|
|
202
|
+
Ink::Database.database.delete_all_links self, Ink::Model.classname(k), v
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
pkvalue = instance_variable_get "@#{self.class.primary_key[0]}"
|
207
|
+
Ink::Database.database.remove self.class.name, "WHERE `#{self.class.primary_key[0]}`=#{(pkvalue.is_a?(Numeric)) ? pkvalue : "\'#{pkvalue}\'"};"
|
208
|
+
end
|
209
|
+
|
210
|
+
# Class method
|
211
|
+
#
|
212
|
+
# This will create SQL statements for creating the
|
213
|
+
# database tables. 'fields' method is mandatory for
|
214
|
+
# this, and 'foreign' is optional.
|
215
|
+
# [returns:] Array of SQL statements
|
216
|
+
def self.create
|
217
|
+
result = Array.new
|
218
|
+
raise NotImplementedError.new("Cannot create a Database without field definitions") if not self.respond_to? :fields
|
219
|
+
|
220
|
+
string = "CREATE TABLE #{Model::str_to_tablename(self.name)} ("
|
221
|
+
mfk = self.foreign_key
|
222
|
+
fields = self.fields
|
223
|
+
for i in 0...fields.keys.length
|
224
|
+
k = fields.keys[i]
|
225
|
+
string += "`#{k}` #{fields[k]*" "}" if k != self.primary_key[0]
|
226
|
+
string += "#{Ink::Database.database.primary_key_autoincrement(k)*" "}" if k == self.primary_key[0]
|
227
|
+
string += "," if i < fields.keys.length - 1
|
228
|
+
end
|
229
|
+
|
230
|
+
if self.respond_to? :foreign
|
231
|
+
foreign = self.foreign
|
232
|
+
for i in 0...foreign.keys.length
|
233
|
+
k = foreign.keys[i]
|
234
|
+
v = foreign[k]
|
235
|
+
fk = Model::classname(k).foreign_key
|
236
|
+
string += ",`#{fk[0]}` #{fk[1]}" if fk.length > 0 and (v == "one_many" or (v == "one_one" and (self.name <=> k) < 0))
|
237
|
+
|
238
|
+
if mfk.length > 0 and fk.length > 1 and v == "many_many" and (self.name <=> k) < 0
|
239
|
+
result.push "CREATE TABLE #{Model::str_to_tablename(self.name)}_#{Model::str_to_tablename(k)} (#{Ink::Database.database.primary_key_autoincrement*" "}, `#{mfk[0]}` #{mfk[1]}, `#{fk[0]}` #{fk[1]});"
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
string += ");"
|
244
|
+
result.push string
|
245
|
+
result
|
246
|
+
end
|
247
|
+
|
248
|
+
# Class method
|
249
|
+
#
|
250
|
+
# This will check the parent module for existing classnames
|
251
|
+
# that match the input of the str parameter.
|
252
|
+
# [param str:] some string
|
253
|
+
# [returns:] valid classname or nil
|
254
|
+
def self.str_to_classname(str)
|
255
|
+
((Module.const_get str.capitalize).is_a? Class) ? str.capitalize : nil
|
256
|
+
end
|
257
|
+
|
258
|
+
# Class method
|
259
|
+
#
|
260
|
+
# This will check the parent module for existing classnames
|
261
|
+
# that match the input of the str parameter. Once found, it
|
262
|
+
# converts the string into the matching tablename.
|
263
|
+
# [param str:] some string
|
264
|
+
# [returns:] valid tablename or nil
|
265
|
+
def self.str_to_tablename(str)
|
266
|
+
((Module.const_get str.capitalize).is_a? Class) ? str.downcase : nil
|
267
|
+
end
|
268
|
+
|
269
|
+
# Class method
|
270
|
+
#
|
271
|
+
# This will check the parent module for existing classnames
|
272
|
+
# that match the input of the str parameter. Once found, it
|
273
|
+
# returns the class, not the string of the class.
|
274
|
+
# [param str:] some string
|
275
|
+
# [returns:] valid class or nil
|
276
|
+
def self.classname(str)
|
277
|
+
((Module.const_get str.capitalize).is_a? Class) ? (Module.const_get str.capitalize) : nil
|
278
|
+
end
|
279
|
+
|
280
|
+
# Class method
|
281
|
+
#
|
282
|
+
# This will find the primary key, as defined in the fields class
|
283
|
+
# method.
|
284
|
+
# [returns:] Array of the form: key name, key type or empty
|
285
|
+
def self.primary_key
|
286
|
+
if self.respond_to? :fields
|
287
|
+
pk = nil
|
288
|
+
pktype = nil
|
289
|
+
self.fields.each do |k,v|
|
290
|
+
if v.is_a?(String) and v == "PRIMARY KEY"
|
291
|
+
pk = k
|
292
|
+
pktype = Ink::Database.database.primary_key_autoincrement(k)[1]
|
293
|
+
end
|
294
|
+
end
|
295
|
+
return [pk, pktype]
|
296
|
+
end
|
297
|
+
return []
|
298
|
+
end
|
299
|
+
|
300
|
+
# Class method
|
301
|
+
#
|
302
|
+
# This will create the foreign key from the defined primary key
|
303
|
+
# [returns:] Array of the form: key name, key type or empty
|
304
|
+
def self.foreign_key
|
305
|
+
pk = self.primary_key
|
306
|
+
return (pk) ? ["#{self.str_to_tablename(self.name)}_#{pk[0]}", pk[1]] : []
|
307
|
+
end
|
308
|
+
|
309
|
+
end
|
310
|
+
|
311
|
+
end
|