webink 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|