Class: M4DBI::Model

Inherits:
Object show all
Extended by:
Enumerable
Defined in:
lib/m4dbi/model.rb

Constant Summary

M4DBI_UNASSIGNED =
'__m4dbi_unassigned__'

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(row = Hash.new) ⇒ Model

——————- :nodoc:



355
356
357
358
359
360
361
362
363
# File 'lib/m4dbi/model.rb', line 355

def initialize( row = Hash.new )
  if ! row.respond_to?( "[]".to_sym ) || ! row.respond_to?( "[]=".to_sym )
    raise M4DBI::Error.new( "Attempted to instantiate M4DBI::Model with an invalid argument (#{row.inspect}).  (Expecting something accessible with [] and []= .)" )
  end
  # if caller[ 1 ] !~ %r{/m4dbi/model\.rb:}
    # warn "Do not call M4DBI::Model#new directly; use M4DBI::Model#create instead."
  # end
  @row = row
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method, *args) ⇒ Object



369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
# File 'lib/m4dbi/model.rb', line 369

def method_missing( method, *args )
  begin
    @row.send( method, *args )
  rescue NoMethodError => e
    if e.backtrace.grep /method_missing/
      # Prevent infinite recursion
      self_str = 'model object'
    elsif self.respond_to? :to_s
      self_str = self.to_s
    elsif self.respond_to? :inspect
      self_str = self.inspect
    elsif self.respond_to? :class
      self_str = "#{self.class} object"
    else
      self_str = "instance of unknown model"
    end

    raise NoMethodError.new(
      "undefined method '#{method}' for #{self_str}",
      method,
      args
    )
  end
end

Class Method Details

.[](first_arg, *args) ⇒ Object



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/m4dbi/model.rb', line 15

def self.[]( first_arg, *args )
  if args.size == 0
    case first_arg
      when Hash
        clause, values = first_arg.to_where_clause
      when NilClass
        clause = pk_clause
        values = [ first_arg ]
      else # single value
        clause = pk_clause
        values = Array( first_arg )
    end
  else
    clause = pk_clause
    values = [ first_arg ] + args
  end

  sql = "SELECT * FROM #{table} WHERE #{clause}"
  stm = prepare(sql)
  row = stm.select_one(*values)
  stm.finish

  if row
    self.new( row )
  end
end

.after_create(&block) ⇒ Object



263
264
265
# File 'lib/m4dbi/model.rb', line 263

def self.after_create(&block)
  hooks[:after_create] << block
end

.after_delete(&block) ⇒ Object



283
284
285
# File 'lib/m4dbi/model.rb', line 283

def self.after_delete(&block)
  hooks[:after_delete] << block
end

.after_update(&block) ⇒ Object



271
272
273
# File 'lib/m4dbi/model.rb', line 271

def self.after_update(&block)
  hooks[:after_update] << block
end

.allObject



101
102
103
104
105
106
# File 'lib/m4dbi/model.rb', line 101

def self.all
  stm = prepare("SELECT * FROM #{table}")
  records = self.from_rows( stm.select_all )
  stm.finish
  records
end

.before_delete(&block) ⇒ Object



279
280
281
# File 'lib/m4dbi/model.rb', line 279

def self.before_delete(&block)
  hooks[:before_delete] << block
end

.cached_fetch(cache_id, *args) ⇒ Object

Acts like self.[] (read only), except it keeps a cache of the fetch results in memory for the lifetime of the thread. Useful for applications like web apps which create a new thread for each HTTP request.

Parameters:

  • cache_id (String)

    A unique key identifying the cache to use.



46
47
48
49
50
51
52
53
# File 'lib/m4dbi/model.rb', line 46

def self.cached_fetch( cache_id, *args )
  if args.size > 1
    self[*args]
  else
    cache = Thread.current["m4dbi_cache_#{cache_id}_#{self.table}"] ||= Hash.new
    cache[*args] ||= self[*args]
  end
end

.countObject



122
123
124
125
126
127
# File 'lib/m4dbi/model.rb', line 122

def self.count
  stm = prepare("SELECT COUNT(*) FROM #{table}")
  retval = stm.select_column.to_i
  stm.finish
  retval
end

.create(hash = {}) ⇒ Object



129
130
131
132
133
134
135
136
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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/m4dbi/model.rb', line 129

def self.create( hash = {} )
  if block_given?
    struct = Struct.new( *( columns.collect { |c| c[ 'name' ].to_sym } ) )
    row = struct.new( *( [ M4DBI_UNASSIGNED ] * columns.size ) )
    yield row
    hash = {}
    row.members.each do |k|
      if row[ k ] != M4DBI_UNASSIGNED
        hash[ k ] = row[ k ]
      end
    end
  end

  keys = hash.keys
  cols = keys.join( ',' )
  values = keys.map { |key| hash[ key ] }
  value_placeholders = values.map { |v| '?' }.join( ',' )
  rec = nil
  num_inserted = 0

  dbh.transaction do |dbh_|
    if keys.empty? && defined?( RDBI::Driver::PostgreSQL ) && RDBI::Driver::PostgreSQL === dbh.driver
      sql = "INSERT INTO #{table} DEFAULT VALUES"
    else
      sql = "INSERT INTO #{table} ( #{cols} ) VALUES ( #{value_placeholders} )"
    end
    stm = prepare(sql)
    num_inserted = stm.execute(*values).affected_count
    stm.finish
    if num_inserted > 0
      pk_hash = hash.slice( *(
        self.pk.map { |pk_col| pk_col.to_sym }
      ) )
      if pk_hash.empty?
        pk_hash = hash.slice( *(
          self.pk.map { |pk_col| pk_col.to_s }
        ) )
      end
      if ! pk_hash.empty?
        rec = self.one_where( pk_hash )
      else
        begin
          rec = last_record( dbh_ )
        rescue NoMethodError => e
          # ignore
          #puts "not implemented: #{e.message}"
        end
      end
    end
  end

  if hooks[:active] && num_inserted > 0
    hooks[:after_create].each do |block|
      hooks[:active] = false
      block.yield rec
      hooks[:active] = true
    end
  end

  rec
end

.each(&block) ⇒ Object

TODO: Perhaps we'll use cursors for Model#each.



109
110
111
# File 'lib/m4dbi/model.rb', line 109

def self.each( &block )
  self.all.each( &block )
end

.find_or_create(hash = nil) ⇒ Object



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/m4dbi/model.rb', line 191

def self.find_or_create( hash = nil )
  item = nil
  error = M4DBI::Error.new( "Failed to find_or_create( #{hash.inspect} )" )
  item = self.one_where( hash )
  if item.nil?
    item =
      begin
        self.create( hash )
      rescue Exception => error
        self.one_where( hash )
      end
  end
  if item
    item
  else
    raise error
  end
end

.from_rows(rows) ⇒ Object



61
62
63
# File 'lib/m4dbi/model.rb', line 61

def self.from_rows( rows )
  rows.map { |r| self.new( r ) }
end

.many_to_many(model1, model2, m1_as, m2_as, join_table, m1_fk, m2_fk) ⇒ Object

Example:

M4DBI::Model.many_to_many(
  @m_author, @m_fan, :authors_liked, :fans, :authors_fans, :author_id, :fan_id
)
her_fans = some_author.fans
favourite_authors = fan.authors_liked


317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/m4dbi/model.rb', line 317

def self.many_to_many( model1, model2, m1_as, m2_as, join_table, m1_fk, m2_fk )
  model1.class_def( m2_as.to_sym ) do
    model2.select_all(
      %{
        SELECT
          m2.*
        FROM
          #{model2.table} m2,
          #{join_table} j
        WHERE
          j.#{m1_fk} = ?
          AND m2.id = j.#{m2_fk}
      },
      # TODO: m2.id?  Should be m2.pk or something
      pk
    )
  end

  model2.class_def( m1_as.to_sym ) do
    model1.select_all(
      %{
        SELECT
          m1.*
        FROM
          #{model1.table} m1,
          #{join_table} j
        WHERE
          j.#{m2_fk} = ?
          AND m1.id = j.#{m1_fk}
      },
      # TODO: Should be m1.pk not m1.id
      pk
    )
  end
end

.oneObject



113
114
115
116
117
118
119
120
# File 'lib/m4dbi/model.rb', line 113

def self.one
  stm = prepare("SELECT * FROM #{table} LIMIT 1")
  row = stm.select_one
  stm.finish
  if row
    self.new( row )
  end
end

.one_to_many(the_one, the_many, many_as, one_as, the_one_fk) ⇒ Object

Example:

M4DBI::Model.one_to_many( Author, Post, :posts, :author, :author_id )
her_posts = some_author.posts
the_author = some_post.author


299
300
301
302
303
304
305
306
307
308
309
# File 'lib/m4dbi/model.rb', line 299

def self.one_to_many( the_one, the_many, many_as, one_as, the_one_fk )
  the_one.class_def( many_as.to_sym ) do
    M4DBI::Collection.new( self, the_many, the_one_fk )
  end
  the_many.class_def( one_as.to_sym ) do
    the_one[ @row[ the_one_fk ] ]
  end
  the_many.class_def( "#{one_as}=".to_sym ) do |new_one|
    send( "#{the_one_fk}=".to_sym, new_one.pk )
  end
end

.one_where(conditions, *args) ⇒ Object



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/m4dbi/model.rb', line 83

def self.one_where( conditions, *args )
  case conditions
    when String
      sql = "SELECT * FROM #{table} WHERE #{conditions} LIMIT 1"
      params = args
    when Hash
      cond, params = conditions.to_where_clause
      sql = "SELECT * FROM #{table} WHERE #{cond} LIMIT 1"
  end

  stm = prepare(sql)
  row = stm.select_one( *params )
  stm.finish
  if row
    self.new( row )
  end
end

.pk_clauseObject



55
56
57
58
59
# File 'lib/m4dbi/model.rb', line 55

def self.pk_clause
  pk.
    map { |col| "#{col} = ?" }.
    join( ' AND ' )
end

.prepare(sql) ⇒ Object



11
12
13
# File 'lib/m4dbi/model.rb', line 11

def self.prepare( sql )
  dbh.prepare(sql)
end

.remove_after_create_hooksObject



267
268
269
# File 'lib/m4dbi/model.rb', line 267

def self.remove_after_create_hooks
  hooks[:after_create].clear
end

.remove_after_delete_hooksObject



291
292
293
# File 'lib/m4dbi/model.rb', line 291

def self.remove_after_delete_hooks
  hooks[:after_delete].clear
end

.remove_after_update_hooksObject



275
276
277
# File 'lib/m4dbi/model.rb', line 275

def self.remove_after_update_hooks
  hooks[:after_update].clear
end

.remove_before_delete_hooksObject



287
288
289
# File 'lib/m4dbi/model.rb', line 287

def self.remove_before_delete_hooks
  hooks[:before_delete].clear
end

.select_all(sql, *binds) ⇒ Object Also known as: s



210
211
212
213
214
215
216
217
# File 'lib/m4dbi/model.rb', line 210

def self.select_all( sql, *binds )
  stm = prepare(sql)
  records = self.from_rows(
    stm.select_all( *binds )
  )
  stm.finish
  records
end

.select_one(sql, *binds) ⇒ Object Also known as: s1



219
220
221
222
223
224
225
226
# File 'lib/m4dbi/model.rb', line 219

def self.select_one( sql, *binds )
  stm = prepare(sql)
  row = stm.select_one( *binds )
  stm.finish
  if row
    self.new( row )
  end
end

.update(where_hash_or_clause, set_hash) ⇒ Object



233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/m4dbi/model.rb', line 233

def self.update( where_hash_or_clause, set_hash )
  where_clause = nil
  set_clause = nil
  where_params = nil

  if where_hash_or_clause.respond_to? :keys
    where_clause, where_params = where_hash_or_clause.to_where_clause
  else
    where_clause = where_hash_or_clause
    where_params = []
  end

  set_clause, set_params = set_hash.to_set_clause
  params = set_params + where_params
  stm = prepare("UPDATE #{table} SET #{set_clause} WHERE #{where_clause}")
  result = stm.execute( *params )
  stm.finish
  result
end

.update_one(*args) ⇒ Object



253
254
255
256
257
258
259
260
261
# File 'lib/m4dbi/model.rb', line 253

def self.update_one( *args )
  set_clause, set_params = args[ -1 ].to_set_clause
  pk_values = args[ 0..-2 ]
  params = set_params + pk_values
  stm = prepare("UPDATE #{table} SET #{set_clause} WHERE #{pk_clause}")
  result = stm.execute( *params )
  stm.finish
  result
end

.where(conditions, *args) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/m4dbi/model.rb', line 65

def self.where( conditions, *args )
  case conditions
    when String
      sql = "SELECT * FROM #{table} WHERE #{conditions}"
      params = args
    when Hash
      cond, params = conditions.to_where_clause
      sql = "SELECT * FROM #{table} WHERE #{cond}"
  end

  stm = prepare(sql)
  rows = self.from_rows(
    stm.select_all(*params)
  )
  stm.finish
  rows
end

Instance Method Details

#==(other) ⇒ Object



421
422
423
# File 'lib/m4dbi/model.rb', line 421

def ==( other )
  other and ( pk == other.pk )
end

#deleteObject

Returns true iff the record and only the record was successfully deleted.



456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
# File 'lib/m4dbi/model.rb', line 456

def delete
  if self.class.hooks[:active]
    self.class.hooks[:before_delete].each do |block|
      self.class.hooks[:active] = false
      block.yield self
      self.class.hooks[:active] = true
    end
  end

  st = prepare("DELETE FROM #{table} WHERE #{pk_clause}")
  num_deleted = st.execute( *pk_values ).affected_count
  st.finish
  if num_deleted != 1
    false
  else
    if self.class.hooks[:active]
      self.class.hooks[:after_delete].each do |block|
        self.class.hooks[:active] = false
        block.yield self
        self.class.hooks[:active] = true
      end
    end
    true
  end
end

#eql?(other) ⇒ Boolean

Returns:

  • (Boolean)


429
430
431
# File 'lib/m4dbi/model.rb', line 429

def eql?( other )
  hash == other.hash
end

#hashObject



425
426
427
# File 'lib/m4dbi/model.rb', line 425

def hash
  "#{self.class.hash}#{pk}".to_i
end

#pkObject

Returns a single value for single-column primary keys, returns an Array for multi-column primary keys.



396
397
398
399
400
401
402
# File 'lib/m4dbi/model.rb', line 396

def pk
  if pk_columns.size == 1
    @row[ pk_columns[ 0 ] ]
  else
    pk_values
  end
end

#pk_clauseObject



415
416
417
418
419
# File 'lib/m4dbi/model.rb', line 415

def pk_clause
  pk_columns.map { |col|
    "#{col} = ?"
  }.join( ' AND ' )
end

#pk_columnsObject



411
412
413
# File 'lib/m4dbi/model.rb', line 411

def pk_columns
  self.class.pk
end

#pk_valuesObject

Always returns an Array of values, even for single-column primary keys.



405
406
407
408
409
# File 'lib/m4dbi/model.rb', line 405

def pk_values
  pk_columns.map { |col|
    @row[ col ]
  }
end

#prepare(sql) ⇒ Object



365
366
367
# File 'lib/m4dbi/model.rb', line 365

def prepare( sql )
  dbh.prepare(sql)
end

#saveObject

save does nothing. It exists to provide compatibility with other ORMs.



483
484
485
# File 'lib/m4dbi/model.rb', line 483

def save
  nil
end

#save!Object



486
487
488
# File 'lib/m4dbi/model.rb', line 486

def save!
  nil
end

#set(hash) ⇒ Object



433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
# File 'lib/m4dbi/model.rb', line 433

def set( hash )
  set_clause, set_params = hash.to_set_clause
  set_params << pk
  state_before = self.to_h
  st = prepare("UPDATE #{table} SET #{set_clause} WHERE #{pk_clause}")
  num_updated = st.execute( *set_params ).affected_count
  st.finish  if defined?( RDBI::Driver::PostgreSQL ) && RDBI::Driver::PostgreSQL === dbh.driver
  if num_updated > 0
    hash.each do |key,value|
      @row[ key ] = value
    end
    if self.class.hooks[:active]
      self.class.hooks[:after_update].each do |block|
        self.class.hooks[:active] = false
        block.yield state_before, self
        self.class.hooks[:active] = true
      end
    end
  end
  num_updated
end

#to_hObject



490
491
492
493
494
495
496
497
# File 'lib/m4dbi/model.rb', line 490

def to_h
  h = Hash.new
  self.class.columns.each do |col|
    col_name = col['name'].to_s
    h[col_name] = @row[col_name]
  end
  h
end