|
| 1 | +# coding: utf-8 |
| 2 | +require 'forwardable' |
| 3 | +require 'active_support/core_ext/hash/indifferent_access' |
| 4 | +require_relative 'dsl/destination' |
| 5 | + |
| 6 | +module RedisCounters |
| 7 | + module Dumpers |
| 8 | + # Класс представляет конечную точку сохранения данных счетчика. |
| 9 | + # |
| 10 | + # Описывает в какую модель (таблицу), какие поля имеющиеся в распоряжении дампера, |
| 11 | + # должны быть сохранены и каким образом. |
| 12 | + # |
| 13 | + # По сути, мерджит указанные поля из temp - таблицы, дампера |
| 14 | + # в указанную таблицу. |
| 15 | + # |
| 16 | + # Может использоваться как напрямую так и с помощью DSL (см. модуль RedisCounters::Dumpers::Dsl::Destination). |
| 17 | + class Destination |
| 18 | + extend Forwardable |
| 19 | + include ::RedisCounters::Dumpers::Dsl::Destination |
| 20 | + |
| 21 | + # Ссылка на родительский движек - дампер. |
| 22 | + attr_accessor :engine |
| 23 | + |
| 24 | + # Модель, в таблицу, которой будет производится мердж данных, AR::Model. |
| 25 | + attr_accessor :model |
| 26 | + |
| 27 | + # Список полей, из доступных дамперу, которые необходимо сохранить, Array. |
| 28 | + attr_accessor :fields |
| 29 | + |
| 30 | + # Список полей, по комбинации которых, будет происходить определение существования записи, |
| 31 | + # при мердже данных, Array. |
| 32 | + attr_accessor :key_fields |
| 33 | + |
| 34 | + # Список полей, которые будет инкрементированы при обновлении существующей записи, Array. |
| 35 | + attr_accessor :increment_fields |
| 36 | + |
| 37 | + # Карта полей - карта псевдонимов полей, Hash. |
| 38 | + # Названия полей в целевой таблице, могут отличаться от названий полей дампера. |
| 39 | + # Для сопоставления полей целевой таблицы и дампера, необходимо заполнить карту соответствия. |
| 40 | + # Карта, заполняется только для тех полей, названия которых отличаются. |
| 41 | + # Во всех свойствах, содержащий указания полей: fields, key_fields, increment_fields, conditions |
| 42 | + # используются имена конечных полей целевой таблицы. |
| 43 | + # |
| 44 | + # Example: |
| 45 | + # fields_map = {:pages => :value, :date => :start_month_date} |
| 46 | + # |
| 47 | + # Означает, что целевое поле :pages, указывает на поле :value, дампера, |
| 48 | + # а целевое поле :date, указывает на поле :start_month_date, дампера. |
| 49 | + attr_accessor :fields_map |
| 50 | + |
| 51 | + # Список дополнительных условий, которые применяются при обновлении целевой таблицы, Array of String. |
| 52 | + # Каждое условие представляет собой строку - часть SQL выражения, которое может включать именованные |
| 53 | + # параметры из числа доступных в хеше оббщих параметров дампера: engine.common_params. |
| 54 | + # Условия соеденяются через AND. |
| 55 | + attr_accessor :conditions |
| 56 | + |
| 57 | + def initialize(engine) |
| 58 | + @engine = engine |
| 59 | + @fields_map = HashWithIndifferentAccess.new |
| 60 | + @conditions = [] |
| 61 | + end |
| 62 | + |
| 63 | + def merge |
| 64 | + target_fields = fields.join(', ') |
| 65 | + |
| 66 | + sql = <<-SQL |
| 67 | + WITH |
| 68 | + source AS |
| 69 | + ( |
| 70 | + SELECT #{selected_fields_expression} |
| 71 | + FROM #{source_table} |
| 72 | + ), |
| 73 | + updated AS |
| 74 | + ( |
| 75 | + UPDATE #{target_table} target |
| 76 | + SET |
| 77 | + #{updating_expression} |
| 78 | + FROM source |
| 79 | + WHERE #{matching_expression} |
| 80 | + #{extra_conditions} |
| 81 | + RETURNING target.* |
| 82 | + ) |
| 83 | + INSERT INTO #{target_table} (#{target_fields}) |
| 84 | + SELECT #{target_fields} |
| 85 | + FROM source |
| 86 | + WHERE NOT EXISTS ( |
| 87 | + SELECT 1 |
| 88 | + FROM updated target |
| 89 | + WHERE #{matching_expression} |
| 90 | + #{extra_conditions} |
| 91 | + ) |
| 92 | + SQL |
| 93 | + |
| 94 | + sql = model.send(:sanitize_sql, [sql, engine.common_params]) |
| 95 | + connection.execute sql |
| 96 | + end |
| 97 | + |
| 98 | + def_delegator :model, :connection |
| 99 | + def_delegator :model, :quoted_table_name, :target_table |
| 100 | + def_delegator :engine, :temp_table_name, :source_table |
| 101 | + |
| 102 | + protected |
| 103 | + |
| 104 | + def selected_fields_expression |
| 105 | + full_fields_map.map { |target_field, source_field| "#{source_field} as #{target_field}" }.join(', ') |
| 106 | + end |
| 107 | + |
| 108 | + def full_fields_map |
| 109 | + fields_map.reverse_merge(Hash[fields.zip(fields)]) |
| 110 | + end |
| 111 | + |
| 112 | + def updating_expression |
| 113 | + increment_fields.map { |field| "#{field} = COALESCE(target.#{field}, 0) + source.#{field}" }.join(', ') |
| 114 | + end |
| 115 | + |
| 116 | + def matching_expression |
| 117 | + source_key_fields = key_fields.map { |field| "source.#{field}" }.join(', ') |
| 118 | + target_key_fields = key_fields.map { |field| "target.#{field}" }.join(', ') |
| 119 | + "(#{source_key_fields}) = (#{target_key_fields})" |
| 120 | + end |
| 121 | + |
| 122 | + def extra_conditions |
| 123 | + result = conditions.map { |condition| "(#{condition})" }.join(' AND ') |
| 124 | + result.present? ? "AND #{result}" : result |
| 125 | + end |
| 126 | + end |
| 127 | + end |
| 128 | +end |
0 commit comments