Class: INat::Data::Entity

Inherits:
Model
  • Object
show all
Extended by:
INat, App::Logger::DSL
Includes:
INat, App::Logger::DSL
Defined in:
lib/inat/data/entity.rb

Fields collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from App::Logger::DSL

debug, debug, echo, echo, error, error, info, info, log, log, warning, warning

Methods inherited from Model

DDL, api_limit, api_part, api_path, backs, block, field, fields, has_path?, has_table?, #ignore, links, #post_update, #process?, #saved?, table, #to_h

Constructor Details

#initialize(id) ⇒ Entity

Returns a new instance of Entity.



185
186
187
188
189
190
# File 'lib/inat/data/entity.rb', line 185

def initialize id
  super()
  self.id = id
  # @saved = true
  @s_count = 0
end

Instance Attribute Details

#idtype: Integer

Returns the id field.

Returns:

  • (type: Integer)

    the id field



181
# File 'lib/inat/data/entity.rb', line 181

field :id, type: Integer, primary_key: true

Class Method Details

.by_id(id) ⇒ Object



77
78
79
# File 'lib/inat/data/entity.rb', line 77

def by_id id
  fetch(id).first
end

.ddlObject



173
174
175
# File 'lib/inat/data/entity.rb', line 173

def ddl
  "INTEGER REFERENCES #{ self.table } (id)"
end

.fetch(*ids) ⇒ Object



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/inat/data/entity.rb', line 57

def fetch *ids
  return [] if ids.empty?
  # sids = if ids.size > 7
  #   ids.take(7).map(&:to_s) + [ 'and more' ]
  # else
  #   ids.map(&:to_s)
  # end
  # Status::status '[fetch]', "#{ self } : " + sids.join(', ') + ' ...'
  result = ids.map { |id| get id }.filter { |x| x != nil }
  nc_ids = result.select { |e| !e.complete? && !e.process? }.map(&:id)
  read(*nc_ids)
  nc_ids = result.select { |e| !e.complete? && !e.process? }.map(&:id)
  load(*nc_ids)
  nc_ids = result.select { |e| !e.complete? && !e.process? }.map(&:id)
  warning "Some #{ self } IDs were not fetched: #{ nc_ids.join(', ') }!" unless nc_ids.empty?
  # result = [ nil ] if result == []
  # Status::status '[fetch]', "#{ self } : " + sids.join(', ') + ' DONE'
  result
end

.from_db_rows(data) ⇒ Object

TODO: подумать о переименовании



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/inat/data/entity.rb', line 82

def from_db_rows data
  result = []
  data.each do |row|
    id = row['id'] || row[:id]
    raise TypeError, "Invalid data row: no 'id' field!" unless id
    # check.delete id
    entity = get id
    entity.update(from_db: true) do
      fields.each do |_, field|
        case field.kind
        when :value
          name, value = field.from_db row
          if name != nil && value != nil
            entity.send "#{ name }=", value
          end
        when :links
          ids = DB.execute("SELECT #{ field.link_field } FROM #{ field.table_name } WHERE #{ field.back_field } = ?", entity.id).map { |x| x[field.link_field.to_s] }
          entity.send "#{ field.id_field }=", ids
        when :backs
          # TODO: подумать над тем, чтобы сразу загрузить: вынести парсинг отдельно...
          ids = DB.execute("SELECT id FROM #{ field.type.table } WHERE #{ field.back_field } = ?", entity.id).map { |x| x['id'] }
          entity.send "#{ field.id_field }=", ids
        end
      end
    end
    result << entity
  end
  result
end

.load(*ids) ⇒ Object



120
121
122
123
124
# File 'lib/inat/data/entity.rb', line 120

def load *ids
  return [] if ids.empty? || @api_path.nil?
  data = INat::API.get @api_path, @api_part, @api_limit, *ids
  data.map { |obj| parse obj }
end

.load_file(filename) ⇒ Object



126
127
128
129
# File 'lib/inat/data/entity.rb', line 126

def load_file filename
  data = API.load_file filename
  data.map { |obj| parse obj }
end

.parse(src) ⇒ Object

Raises:

  • (TypeError)


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
# File 'lib/inat/data/entity.rb', line 131

def parse src
  return nil if src == nil
  raise TypeError, "Source must be a Hash! (#{ src.inspect })" unless Hash === src
  # if !(Hash === src)
  #   warning "INVALID SOURCE for #{ self }: #{ src.inspect }"
  #   return nil
  # end
  id = src[:id] || src['id']
  raise ArgumentError, "Source must have an Integer 'id' value!", caller unless Integer === id
  fields = self.fields
  entity = self.get id
  entity.update do
    src.each do |key, value|
      key = key.intern if String === key
      field = fields[key] || fields.values.find { |f| f.id_field == key }
      raise ArgumentError, "Field not found in #{ self.name }: '#{ key }'!", caller unless field
      if field.write?
        unless (field.type === value) || (field.id_field == key && Integer === value)
          if field.id_field == key
            # do nothing
          elsif field.type.respond_to?(:parse)
            if Array === value
              if field.kind == :backs
                value = value.map { |v| field.type.parse(v.merge(field.back_field => entity.id)) }
              else
                value = value.map { |v| field.type.parse(v) }
              end
            else
              value = field.type.parse(value)
            end
          else
            raise TypeError, "Invalid '#{ key }' value type: #{ value.inspect }!", caller
          end
        end
        entity.send "#{ key }=", value
      end
    end
  end
  entity
  # entity.save
end

.read(*ids) ⇒ Object



112
113
114
115
116
117
118
# File 'lib/inat/data/entity.rb', line 112

def read *ids
  return [] if ids.empty?
  # check = ids.dup
  # fields = self.fields
  data = DB.execute "SELECT * FROM #{ self.table } WHERE id IN (#{ (['?'] * ids.size).join(',') })", *ids
  from_db_rows data
end

Instance Method Details

#complete?Boolean

Returns:



192
193
194
195
196
# File 'lib/inat/data/entity.rb', line 192

def complete?
  fields = self.class.fields.values.select { |f| f.required? }
  # values = fields.map { |f| [ f.name, send(f.name) ] }
  fields.all? { |f| send(f.name) != nil }
end

#get(id) ⇒ Object (private)



48
49
50
51
52
53
54
55
# File 'lib/inat/data/entity.rb', line 48

private def get id
  return nil if id == nil
  update do
    @entities ||= {}
    @entities[id] ||= new id
    @entities[id]
  end
end

#initObject (private)



29
30
31
# File 'lib/inat/data/entity.rb', line 29

private def init
  @mutex = Mutex::new
end

#saveObject



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/inat/data/entity.rb', line 198

def save
  return self if @saved
  # debug "Save #{ self.class.name } id = #{ self.id } saved = #{ @saved.inspect }"
  @s_count += 1
  debug "Saving count = #{ @s_count } [#{ self.class }: #{ self.id }]" if @s_count > 1
  names = []
  values = []
  links = []
  backs = []
  # update do
    self.class.fields.each do |_, field|
      case field.kind
      when :value
        value = self.send(field.name)
        if INat::Entity === value && value != self # && !value.process?
          value.save
        end
        name, value = field.to_db value
        if name != nil && value != nil
          names << name
          values << value
        end
      when :links
        links << { field: field, values: self.send(field.name) } if field.owned?
      when :backs
        backs << { field: field, values: self.send(field.name) } if field.owned?
      end
    end
  # end
  names = names.flatten
  values = values.flatten
  # DB.transaction do |db|
    DB.execute "INSERT OR REPLACE INTO #{ self.class.table } (#{ names.join(',') }) VALUES (#{ (['?'] * values.size).join(',') });", *values
    @saved = true
    links.each do |link|
      field = link[:field]
      values = link[:values]
      olinks = []
      values.each do |value|
        value.save if value != self
        # DB.execute "INSERT OR REPLACE INTO #{ field.table_name } (#{ field.back_field }, #{ field.link_field }) VALUES (?, ?);", self.id, value.id
        olinks << "INSERT OR REPLACE INTO #{ field.table_name } (#{ field.back_field }, #{ field.link_field }) VALUES (#{ self.id }, #{ value.id });"
      end
      DB.execute_batch olinks.join("\n")
      news = values.map(&:id)
      olds = DB.execute("SELECT #{ field.link_field } as id FROM #{ field.table_name } WHERE #{ field.back_field } = ?;", self.id).map { |r| r['id'] }
      diff = olds.filter { |o| !news.include?(o) }
      if !diff.empty?
        DB.execute "DELETE FROM #{ field.table_name } WHERE #{ field.back_field } = ? AND #{ field.link_field } IN (#{ (['?'] * diff.size).join(',') });",
                    self.id, *diff
      end
    end
    backs.each do |back|
      field = back[:field]
      values = back[:values]
      values.each do |value|
        value.send "#{ field.back_field }=", self.id
        value.save if value != self
      end
      news = values.map(&:id)
      olds = DB.execute("SELECT id FROM #{ field.type.table } WHERE #{ field.back_field } = ?;", self.id).map { |r| r['id'] }
      diff = olds.filter { |o| !news.include?(o) }
      if !diff.empty?
        DB.execute "DELETE FROM #{ field.type.table } WHERE #{ field.back_field } = ? AND id IN (#{ (['?'] * diff.size).join(',') });",
                    self.id, *diff
      end
    end
  # end
  # @saved = true
  # TODO: разобраться и почистить двойное присваивание
  self
end

#to_dbObject



271
272
273
# File 'lib/inat/data/entity.rb', line 271

def to_db
  self.id
end

#updateObject (private)

Raises:

  • (ArgumentError)


33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/inat/data/entity.rb', line 33

private def update
  raise ArgumentError, "Block is required!", caller unless block_given?
  result = nil
  exception = nil
  @mutex.synchronize do
    begin
      result = yield
    rescue Exception => e
      exception = e
    end
  end
  raise exception.class, exception.message, caller, cause: exception if exception
  result
end