Class: Ink::Model

Inherits:
Object
  • Object
show all
Defined in:
lib/webink/model.rb

Overview

Model class

Usage

Models are usually derived from. So let’s assume there is a class called Apple < Ink::Model

apple = Apple.new {:color => "red", :diameter => 4}
apple = Apple.new [ "red", 4 ]

The constructor checks, if there are class methods ‘fields’ and ‘foreign’ defined. If that check is positive, it will match the parameter Hash to the fields, that are set for the database, and throw an exception if fields is lacking an entry (excluded the primary key). The other case just creates an Apple with the Hash as instance variables. An alternate method of creating a new apple is by providing an Array of values in the same order as in the fields definition.

puts apple.color

This prints “red” to the stdout, since getter and setter methods are automatically added for either the Hash, or the fields and foreign keys.

apple.save

You can save your apple by using the save method. New instances will create a new row in the database, and update its primary key. Old instances just update the fields. Relationships are set to nil by default and will not be touched while nil.

treeinstance.apple = [1,2,myapple]
treeinstance.save

To insert relationship data, you can provide them by array, value or reference, so setting treeinstance.apple to 1 is allowed, also to myapple, or an array or a combination. An empty array [] will remove all references. This works both ways, just consider the relationship type, as an apple cannot have more than one tree.

treeinstance.delete

The model provides a convenience method for deletion. It removes all references from relationships, but does not remove the relationships themselves, so you must fetch all related data, and delete them by ‘hand’ if you will.

treeinstance.find_references Apple

This convenience method finds all apples for this tree and makes them available in the accessor. If the Tree-Apple relationship is a *_one, then there is only one object in the accessor, otherwise an Array of objects.

Fields and foreign sample config

class Apple < Ink::Model
  def self.fields
    fields = {
      :id => "PRIMARY KEY"
      :color => [ "VARCHAR", "NOT NULL" ],
      :diameter => [ "NUMERIC", "NOT NULL" ]
    }
    fields
  end
  def self.foreign
    foreign = {
      "Tree" => "one_many"
    }
    foreign
  end
end

Let’s look at this construct. The constructor is inherited from Ink::Model, so are its methods. ‘fields’ defines a Hash of Arrays, that will create the Database table for us. ‘foreign’ handles the contraints to other classes, here it reads: one “Tree” has many Apples, other constructs could be: [one “Tree” has one Apple, many “Tree”s have many Apples, many “Tree”s have one Apple] => [one_one, many_many, many_one] Obviously the Tree class requires a foreign with “Apple” mapped to “many_one” to match this schema.

You can override the automatically generated getters and setters in any Model class you create by just redefining the methods.

Convenience methods

self.primary_key
self.primary_key_type
self.foreign_key
self.foreign_key_type

primary_key is the name of the primary key (default “id”). The foreign_key has a combination of “classname”_“primary_key” (i.e. “apple_id”)

self.class_name

Equivalent to class.name

self.table_name

Generates a table representation of the class. (Apple as “apple” and MyApple as “my_apple”)

self.str_to_classname(str)

Converts a table name to class name. This method takes a string.

self.str_to_tablename(str)

Converts a class name to table name. This method takes a string.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(data) ⇒ Model

Constructor

Keys from the data parameter will be converted into instance variables with getters and setters in place. The data parameter can be an Array of length of the defined fields or without the primary key. The order needs to be the same as the defined fields. The primary key has no setter, but adds a getter called pk for convenience.

param data:

Hash of String => Objects or Array of Objects



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/webink/model.rb', line 137

def initialize(data)
  if self.class.respond_to? :fields
    i = 0
    self.class.fields.each do |k,v|
      if data.is_a? Array
        if data.length < self.class.fields.length - 1 or
            data.length > self.class.fields.length
          raise LoadError.new(<<ERR)
Model cannot be loaded, wrong number or arguments #{data.length} expected
#{self.class.fields.length} or #{self.class.fields.length - 1}
ERR
        end
        if self.class.primary_key != k or
            data.length == self.class.fields.length
          init_field k, data[i]
          i += 1
        else
          init_field self.class.primary_key, nil
        end
      else
        if not data.key?(k.to_s) and self.class.primary_key != k
          raise LoadError.new(<<ERR)
Model cannot be loaded, argument missing: #{k}
ERR
        end
        init_field k, data[k.to_s]
      end
    end
    if self.class.respond_to? :foreign
      self.class.foreign.each do |k,v|
        init_foreign k
      end
    end
  else
    data.each do |k,v|
      init_no_fields k, v
    end
  end
end

Class Method Details

.class_nameObject

Class method

This will retrieve a string-representation of the model name

returns:

valid classname



427
428
429
# File 'lib/webink/model.rb', line 427

def self.class_name
  self.name
end

.classname(str) ⇒ Object

Class method

This will check the parent module for existing classnames that match the input of the str parameter. Once found, it returns the class, not the string of the class.

param str:

some string

returns:

valid class or nil



478
479
480
481
482
483
484
485
486
487
488
489
490
491
# File 'lib/webink/model.rb', line 478

def self.classname(str)
  res = []
  if str[0] =~ /^[a-z]/
    str.scan(/((^|_)([a-z0-9]+))/) { |s|
      if s.length > 0
        res.push(s[2][0].upcase +
          ((s[2].length > 1) ? s[2][1,s[2].length] : ""))
      end
    }
  else
    res.push str
  end
  Module.const_get(res.join).is_a?(Class) ? Module.const_get(res.join) : nil
end

.createObject

Class method

This will create SQL statements for creating the database tables. ‘fields’ method is mandatory for this, and ‘foreign’ is optional.

returns:

Array of SQL statements



381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
# File 'lib/webink/model.rb', line 381

def self.create
  result = Array.new
  if not self.respond_to?(:fields)
    puts "Skipping #{self.name}...."
    return []
  end

  string = "CREATE TABLE #{self.table_name} ("
  mfk = self.foreign_key
  string += self.fields.map do |k,v|
    if k != self.primary_key
      "`#{k}` #{v*" "}"
    else
      "#{Ink::Database.database.primary_key_autoincrement(k)*" "}"
    end
  end.join(",")

  if self.respond_to? :foreign
     tmp = self.foreign.map do |k,v|
       f_class = Ink::Model::classname(k)
       if v == "many_many" and (self.name <=> k) < 0
         result.push <<QUERY
CREATE TABLE #{self.table_name}_#{Ink::Model::str_to_tablename(k)}
(#{Ink::Database.database.primary_key_autoincrement*" "},
`#{self.foreign_key}` #{self.foreign_key_type},
`#{f_class.foreign_key}` #{f_class.foreign_key_type});
QUERY
         nil
       end
       if v == "one_many" or (v == "one_one" and (self.name <=> k) < 0)
         "`#{f_class.foreign_key}` #{f_class.foreign_key_type}"
       else
         nil
       end
     end.compact.join(",")
     string += ",#{tmp}" if not tmp.empty?
  end
  string += ");"
  result.push string
  result
end

.foreign_keyObject

Class method

This will create the foreign key from the defined primary key

returns:

key name or nil



524
525
526
527
# File 'lib/webink/model.rb', line 524

def self.foreign_key
  pk = self.primary_key
  return (pk) ? "#{self.table_name}_#{pk}" : nil
end

.foreign_key_typeObject

Class method

This will find the foreign key type, taken from the primary key in fields.

returns:

key type or nil



534
535
536
# File 'lib/webink/model.rb', line 534

def self.foreign_key_type
  self.primary_key_type
end

.make_safe(value) ⇒ Object

Class method

Similar to making an Object sql safe. Escapes quotes.

param value:

Object

returns:

safe Object



183
184
185
186
187
188
189
190
191
192
193
# File 'lib/webink/model.rb', line 183

def self.make_safe(value)
  if value.nil?
    nil
  elsif value.is_a? String
    value.gsub(/'/, '&#39;')
  elsif value.is_a? Numeric
    value
  else
    "\'#{value}\'"
  end
end

.primary_keyObject

Class method

This will find the primary key, as defined in the fields class method.

returns:

key name or nil



498
499
500
501
502
503
504
# File 'lib/webink/model.rb', line 498

def self.primary_key
  if self.respond_to? :fields
    field = self.fields.select{|k,v| v.is_a?(String) and v == "PRIMARY KEY"}
    return field.keys.first
  end
  nil
end

.primary_key_typeObject

Class method

This will find the primary key type, as defined in the fields class method.

returns:

key type or nil



511
512
513
514
515
516
517
518
# File 'lib/webink/model.rb', line 511

def self.primary_key_type
  if self.respond_to? :fields
    field = self.fields.select{|k,v| v.is_a?(String) and v == "PRIMARY KEY"}
    return Ink::Database.database.
      primary_key_autoincrement(field.keys.first)[1]
  end
  nil
end

.str_to_classname(str) ⇒ Object

Class method

This will check the parent module for existing classnames that match the input of the str parameter.

param str:

some string

returns:

valid classname or nil



445
446
447
448
449
450
451
452
453
454
# File 'lib/webink/model.rb', line 445

def self.str_to_classname(str)
  res = []
  str.scan(/((^|_)([a-z0-9]+))/) { |s|
    if s.length > 0
      res.push(s[2][0].upcase +
        ((s[2].length > 1) ? s[2][1,s[2].length] : ""))
    end
  }
  Module.const_get(res.join).is_a?(Class) ? res.join : nil
end

.str_to_tablename(str) ⇒ Object

Class method

This will check the parent module for existing classnames that match the input of the str parameter. Once found, it converts the string into the matching tablename.

param str:

some string

returns:

valid tablename or nil



463
464
465
466
467
468
469
# File 'lib/webink/model.rb', line 463

def self.str_to_tablename(str)
  res = []
  str.scan(/([A-Z][a-z0-9]*)/) { |s|
    res.push (res.length>0) ? "_" + s.join.downcase : s.join.downcase
  }
  Module.const_get(str).is_a?(Class) ? res.join : nil
end

.table_nameObject

Class method

This will retrieve a tablename-representation of the model name

returns:

valid tablename



435
436
437
# File 'lib/webink/model.rb', line 435

def self.table_name
  self.str_to_tablename self.name
end

Instance Method Details

#deleteObject

Instance method

Deletes the data from the database, essentially making the instance obsolete. Disregard from using the instance anymore. All links between models will be removed also.



333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/webink/model.rb', line 333

def delete
  if not self.class.respond_to? :fields
  raise NotImplementedError.new(<<ERR)
Cannot delete from Database without field definitions
ERR
  end
  if self.class.respond_to? :foreign
    self.class.foreign.each do |k,v|
      Ink::Database.database.delete_all_links(self,
        Ink::Model.classname(k), v)
    end
  end

  pkvalue = instance_variable_get "@#{self.class.primary_key}"
  Ink::Database.database.remove self.class.name, <<QUERY
WHERE `#{self.class.primary_key}`=#{
(pkvalue.is_a?(Numeric)) ? pkvalue : "\'#{pkvalue}\'"
}
QUERY
end

#find_references(foreign_class) ⇒ Object

Instance method

Queries the database for foreign keys and attaches them to the matching foreign accessor

param foreign_class:

Defines the foreign class name or class



359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'lib/webink/model.rb', line 359

def find_references(foreign_class)
  c = (foreign_class.is_a? Class) ? foreign_class :
    Ink::Model.classname(foreign_class)
  relationship = self.class.foreign[c.class_name]
  if relationship
    result_array = (relationship == "many_many") ?
      Ink::Database.database.find_union(self.class, self.pk, c) :
      Ink::Database.database.find_references(self.class, self.pk, c)
    instance_variable_set("@#{c.table_name}",
      (relationship =~ /^one_/) ? result_array.first : result_array)
    true
  else
    false
  end
end

#saveObject

Instance method

Save the instance to the database. Set all foreign sets to nil if you do not want to change them. Old references are automatically removed.



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/webink/model.rb', line 271

def save
  if not self.class.respond_to?(:fields)
  raise NotImplementedError.new(<<ERR)
Cannot save to Database without field definitions
ERR
  end
  string = Array.new
  keystring = Array.new
  valuestring = Array.new
  pkvalue = nil
  self.class.fields.each do |k,v|
    value = instance_variable_get "@#{k}"
    value = "NULL" if value.nil?
    if k != self.class.primary_key
      string.push "`#{k}`=#{(value.is_a?(Numeric)) ? value :
        "\'#{value}\'"}"
      keystring.push "`#{k}`"
      valuestring.push "#{(value.is_a?(Numeric)) ? value : "\'#{value}\'"}"
    else
      pkvalue = "WHERE `#{self.class.primary_key}`=#{
        (value.is_a?(Numeric)) ? value : "\'#{value}\'"
        }"
    end
  end
  if pkvalue
    response = Ink::Database.database.find self.class, pkvalue
    if response.empty?
      Ink::Database.database.query <<QUERY
INSERT INTO #{self.class.table_name}
(#{keystring * ","}) VALUES
(#{valuestring * ","});
QUERY
      pk = Ink::Database.database.last_inserted_pk(self.class)
      if pk
        instance_variable_set("@#{self.class.primary_key}",
          pk.is_a?(Numeric) ? pk : "\'#{pk}\'")
      end
    else
      Ink::Database.database.query <<QUERY
UPDATE #{self.class.table_name} SET #{string * ","} #{pkvalue};
QUERY
    end
  end

  if self.class.respond_to? :foreign
    self.class.foreign.each do |k,v|
      value = instance_variable_get "@#{self.class.str_to_tablename(k)}"
      if value
        Ink::Database.database.delete_all_links(self,
          Ink::Model.classname(k), v)
        Ink::Database.database.create_all_links(self,
          Ink::Model.classname(k), v, value)
      end
    end
  end
end