rsim/oracle-enhanced

View on GitHub
lib/active_record/connection_adapters/oracle_enhanced/procedures.rb

Summary

Maintainability
A
3 hrs
Test Coverage
# frozen_string_literal: true

require "active_support"

module ActiveRecord # :nodoc:
  # Custom create, update, delete methods functionality.
  #
  # Example:
  #
  #   class Employee < ActiveRecord::Base
  #     include ActiveRecord::OracleEnhancedProcedures
  #
  #     set_create_method do
  #       plsql.employees_pkg.create_employee(
  #         :p_first_name => first_name,
  #         :p_last_name => last_name,
  #         :p_employee_id => nil
  #       )[:p_employee_id]
  #     end
  #
  #     set_update_method do
  #       plsql.employees_pkg.update_employee(
  #         :p_employee_id => id,
  #         :p_first_name => first_name,
  #         :p_last_name => last_name
  #       )
  #     end
  #
  #     set_delete_method do
  #       plsql.employees_pkg.delete_employee(
  #         :p_employee_id => id
  #       )
  #     end
  #   end
  #
  module OracleEnhancedProcedures # :nodoc:
    module ClassMethods
      # Specify custom create method which should be used instead of Rails generated INSERT statement.
      # Provided block should return ID of new record.
      # Example:
      #   set_create_method do
      #     plsql.employees_pkg.create_employee(
      #       :p_first_name => first_name,
      #       :p_last_name => last_name,
      #       :p_employee_id => nil
      #     )[:p_employee_id]
      #   end
      def set_create_method(&block)
        self.custom_create_method = block
      end

      # Specify custom update method which should be used instead of Rails generated UPDATE statement.
      # Example:
      #   set_update_method do
      #     plsql.employees_pkg.update_employee(
      #       :p_employee_id => id,
      #       :p_first_name => first_name,
      #       :p_last_name => last_name
      #     )
      #   end
      def set_update_method(&block)
        self.custom_update_method = block
      end

      # Specify custom delete method which should be used instead of Rails generated DELETE statement.
      # Example:
      #   set_delete_method do
      #     plsql.employees_pkg.delete_employee(
      #       :p_employee_id => id
      #     )
      #   end
      def set_delete_method(&block)
        self.custom_delete_method = block
      end
    end

    def self.included(base)
      base.class_eval do
        extend ClassMethods
        class_attribute :custom_create_method
        class_attribute :custom_update_method
        class_attribute :custom_delete_method
      end
    end

    def destroy # :nodoc:
      # check if class has custom delete method
      if self.class.custom_delete_method
        # wrap destroy in transaction
        with_transaction_returning_status do
          # run before/after callbacks defined in model
          run_callbacks(:destroy) { destroy_using_custom_method }
        end
      else
        super
      end
    end

    private
      # Creates a record with custom create method
      # and returns its id.
      def _create_record
        # check if class has custom create method
        if self.class.custom_create_method
          # run before/after callbacks defined in model
          run_callbacks(:create) do
            # timestamp
            if self.record_timestamps
              current_time = current_time_from_proper_timezone

              all_timestamp_attributes_in_model.each do |column|
                if respond_to?(column) && respond_to?("#{column}=") && self.send(column).nil?
                  write_attribute(column.to_s, current_time)
                end
              end
            end
            # run
            create_using_custom_method
          end
        else
          super
        end
      end

      def create_using_custom_method
        log_custom_method("custom create method", "#{self.class.name} Create") do
          self.id = instance_eval(&self.class.custom_create_method)
        end
        @new_record = false
        # Starting from ActiveRecord 3.0.3 @persisted is used instead of @new_record
        @persisted = true
        id
      end

      # Updates the associated record with custom update method
      # Returns the number of affected rows.
      def _update_record(attribute_names = @attributes.keys)
        # check if class has custom update method
        if self.class.custom_update_method
          # run before/after callbacks defined in model
          run_callbacks(:update) do
            # timestamp
            if should_record_timestamps?
              current_time = current_time_from_proper_timezone

              timestamp_attributes_for_update_in_model.each do |column|
                column = column.to_s
                next if will_save_change_to_attribute?(column)
                write_attribute(column, current_time)
              end
            end
            # update just dirty attributes
            if partial_updates?
              # Serialized attributes should always be written in case they've been
              # changed in place.
              update_using_custom_method(changed | (attributes.keys & self.class.columns.select { |column| column.is_a?(Type::Serialized) }))
            else
              update_using_custom_method(attributes.keys)
            end
          end
        else
          super
        end
      end

      def update_using_custom_method(attribute_names)
        return 0 if attribute_names.empty?
        log_custom_method("custom update method with #{self.class.primary_key}=#{self.id}", "#{self.class.name} Update") do
          instance_eval(&self.class.custom_update_method)
        end
        1
      end

      # Deletes the record in the database with custom delete method
      # and freezes this instance to reflect that no changes should
      # be made (since they can't be persisted).
      def destroy_using_custom_method
        unless new_record? || @destroyed
          log_custom_method("custom delete method with #{self.class.primary_key}=#{self.id}", "#{self.class.name} Destroy") do
            instance_eval(&self.class.custom_delete_method)
          end
        end

        @destroyed = true
        freeze
      end

      def log_custom_method(*args, &block)
        self.class.connection.send(:log, *args, &block)
      end

      alias_method :update_record, :_update_record if private_method_defined?(:_update_record)
      alias_method :create_record, :_create_record if private_method_defined?(:_create_record)
  end
end