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

- (Model) initialize(row = Hash.new)

——————- :nodoc:



338
339
340
341
342
343
344
345
346
347
# File 'lib/m4dbi/model.rb', line 338

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
  @st = Hash.new
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

- (Object) method_missing(method, *args)



353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/m4dbi/model.rb', line 353

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

+ (Object) [](first_arg, *args)



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
# 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)

  if row
    self.new( row )
  end
end

+ (Object) after_create(&block)



246
247
248
# File 'lib/m4dbi/model.rb', line 246

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

+ (Object) after_delete(&block)



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

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

+ (Object) after_update(&block)



254
255
256
# File 'lib/m4dbi/model.rb', line 254

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

+ (Object) all



97
98
99
100
# File 'lib/m4dbi/model.rb', line 97

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

+ (Object) before_delete(&block)



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

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

+ (Object) cached_fetch(cache_id, *args)

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.



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

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

+ (Object) count



115
116
117
118
# File 'lib/m4dbi/model.rb', line 115

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

+ (Object) create(hash = {})



120
121
122
123
124
125
126
127
128
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
# File 'lib/m4dbi/model.rb', line 120

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
    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

+ (Object) each(&block)

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



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

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

+ (Object) find_or_create(hash = nil)



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/m4dbi/model.rb', line 181

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

+ (Object) from_rows(rows)



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

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

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

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


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
327
328
329
330
331
332
333
334
# File 'lib/m4dbi/model.rb', line 300

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

+ (Object) one



107
108
109
110
111
112
113
# File 'lib/m4dbi/model.rb', line 107

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

+ (Object) one_to_many(the_one, the_many, many_as, one_as, the_one_fk)

Example:

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


282
283
284
285
286
287
288
289
290
291
292
# File 'lib/m4dbi/model.rb', line 282

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

+ (Object) one_where(conditions, *args)



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/m4dbi/model.rb', line 80

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 )
  if row
    self.new( row )
  end
end

+ (Object) pk_clause



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

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

+ (Object) prepare(sql)



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

def self.prepare( sql )
  st[sql] ||= dbh.prepare(sql)
end

+ (Object) remove_after_create_hooks



250
251
252
# File 'lib/m4dbi/model.rb', line 250

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

+ (Object) remove_after_delete_hooks



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

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

+ (Object) remove_after_update_hooks



258
259
260
# File 'lib/m4dbi/model.rb', line 258

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

+ (Object) remove_before_delete_hooks



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

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

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



200
201
202
203
204
205
# File 'lib/m4dbi/model.rb', line 200

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

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



207
208
209
210
211
212
213
# File 'lib/m4dbi/model.rb', line 207

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

+ (Object) update(where_hash_or_clause, set_hash)



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/m4dbi/model.rb', line 220

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}")
  stm.execute( *params )
end

+ (Object) update_one(*args)



238
239
240
241
242
243
244
# File 'lib/m4dbi/model.rb', line 238

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}")
  stm.execute( *params )
end

+ (Object) where(conditions, *args)



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

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)
  self.from_rows(
    stm.select_all(*params)
  )
end

Instance Method Details

- (Object) ==(other)



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

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

- (Object) delete

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



439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
# File 'lib/m4dbi/model.rb', line 439

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
  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

- (Boolean) eql?(other)

Returns:

  • (Boolean)


413
414
415
# File 'lib/m4dbi/model.rb', line 413

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

- (Object) hash



409
410
411
# File 'lib/m4dbi/model.rb', line 409

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

- (Object) pk

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



380
381
382
383
384
385
386
# File 'lib/m4dbi/model.rb', line 380

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

- (Object) pk_clause



399
400
401
402
403
# File 'lib/m4dbi/model.rb', line 399

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

- (Object) pk_columns



395
396
397
# File 'lib/m4dbi/model.rb', line 395

def pk_columns
  self.class.pk
end

- (Object) pk_values

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



389
390
391
392
393
# File 'lib/m4dbi/model.rb', line 389

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

- (Object) prepare(sql)



349
350
351
# File 'lib/m4dbi/model.rb', line 349

def prepare( sql )
  @st[sql] ||= dbh.prepare(sql)
end

- (Object) save

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



465
466
467
# File 'lib/m4dbi/model.rb', line 465

def save
  nil
end

- (Object) save!



468
469
470
# File 'lib/m4dbi/model.rb', line 468

def save!
  nil
end

- (Object) set(hash)



417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
# File 'lib/m4dbi/model.rb', line 417

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
  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

- (Object) to_h



472
473
474
475
476
477
478
479
# File 'lib/m4dbi/model.rb', line 472

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