uses-store-procedures 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ Copyright 2013 Leopold O'Donnell
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,133 @@
1
+ [![Build Status](https://travis-ci.org/leopoldodonnell/uses-stored-procedures.png?branch=master)](https://travis-ci.org/leopoldodonnell/uses-stored-procedures)
2
+ [![Dependency Status](https://gemnasium.com/leopoldodonnell/uses-stored-procedures.png)](https://gemnasium.com/leopoldodonnell/uses-stored-procedures)
3
+ [![Code Climate](https://codeclimate.com/github/leopoldodonnell/uses-stored-procedures.png)](https://codeclimate.com/github/leopoldodonnell/uses-stored-procedures)
4
+
5
+ # UsesStoredProcedures
6
+
7
+ Extends ActiveRecord with the ability to use SQL Stored Procedures in Rails.
8
+
9
+ So, you really need to use stored procedures in your Rails application. This gem extends ActiveRecord with a class method to add to your models or services. Sure stored procedures are *not the Rails way* they are an occasional project necessity. Your reasons may stem from a need to integrate with a legacy database with built-in business rules, or a need to improve performance where other avenues have been exhausted (given available project time). If you need this, *you know who you are*, I don't need to tell you why.
10
+
11
+ ## Version
12
+
13
+ 0.1.0
14
+
15
+ ## Requirements
16
+
17
+ ### Ruby
18
+
19
+ - 1.8.7, 1.9
20
+
21
+ ### Rails
22
+
23
+ - 3.1.x or 3.2.x
24
+
25
+ ### ORM
26
+
27
+ - ActiveRecord
28
+ - Mysql2, MySQL (Tested/Passing)
29
+ - SQLServer (Untested, but should work)
30
+ - PostgreSQL (Untested, unsure - please try it and notify the author)
31
+
32
+ ## Installation
33
+
34
+ The current state of this gem is tested with MySQL, but it also does detection of SQLServer and PostgreSQL which will
35
+ be tested when time permits. Please contact the author if you've gotten it to work before this happens.
36
+
37
+ In your Gemfile
38
+
39
+ gem 'uses-stored-procedures', git => "git://github.com/leopoldodonnell/uses-stored-procedures"
40
+
41
+ Then from your project root:
42
+
43
+ > bundle
44
+
45
+
46
+ ## Using uses_stored_procedures to call stored procedures from Rails
47
+
48
+ **UsesStoredProcedues** extends ActiveRecord::Base with the single class method that takes the name of the procedure as a symbol, and optional parameters to generate a class and instance method that can be called to run the stored procedure.
49
+
50
+ Here's a simple example:
51
+
52
+ ```ruby
53
+ class MyClass < ActiveRecord::Base
54
+ use_stored_proc :get_office_by_country {|row| "#{row.country} #{row.city}, phone: #{row.phone}"}
55
+ end
56
+ ```
57
+
58
+ Which generates:
59
+
60
+ ```ruby
61
+ def self.get_office_by_country(*args, &block)
62
+ # code
63
+ end
64
+
65
+ def get_office_by_country(*args, &block)
66
+ self.class.get_offic_by_country(args, block)
67
+ end
68
+ ```
69
+
70
+ So later you can do the following:
71
+
72
+ ```ruby
73
+ result = MyClass.get_office_by_country 'Canada'
74
+ result.each {|item| puts item}
75
+ ```
76
+
77
+ Note that the object passed to the block is a *UsesStoredProcedures#HashWithAttributes* instance that provides accessors for all of its keys.
78
+
79
+ ### Syntax
80
+
81
+ The method signature for *use_stored_proc* is:
82
+
83
+ ```ruby
84
+ def self.use_stored_proc(method_name, *options, &block)
85
+ ```
86
+
87
+ That then generates a class and instance method named *method_name*.
88
+
89
+ * method_name - is the symbolic name of the stored procedure and is the name given to the generated class and instance methods. By default, the stored procedure that is called will be a stringified version of this method_name. Use the :proc_name option if the method_name won't be matching actual stored procedure name.
90
+
91
+ * options - is an array of arguments that are provided as *:arg_name => value* and where the last argument may be a block, or method name.
92
+
93
+ ####Options
94
+
95
+ * proc_name - if the stored procedure name is different than method_name parameter, provide a string that matches the actual stored procedure name.
96
+
97
+ * filter - the name of a class method that receives an instance of *UsesStoredProcedures#HashWithAttributes* to map returned rows.
98
+
99
+ * block - make the last parameter a block that receive an instance of *UsesStoredProcedures#HashWithAttributes* to map returned rows.
100
+
101
+ #### Generated Methods
102
+
103
+ The generated methods all take a variable number of parameters that are mapped to the stored procedure call and can also take a block that can be used to map the returned rows. By default the returned value will be an array of values that are either instances of *UsesStoredProcedures#HashWithAttributes*, or mapped by the block. Note that passing a block overrides the block or method provided when calling *#use_stored_proc*
104
+
105
+ ### Another Example
106
+
107
+ ```ruby
108
+ require 'uses_stored_procedures'
109
+
110
+ class ClientServices
111
+ include UsesStoredProcedures
112
+
113
+ use_stored_proc :list_inactive_clients, :proc_name => 'GET_CLIENTS_INACTIVE_STATUS', :filter => :make_clients
114
+
115
+ def self.make_clients(item)
116
+ Client.new item
117
+ end
118
+ end
119
+
120
+ client_services = ClientServices.new
121
+
122
+ # ...
123
+
124
+ client_list = client_services.list_inactive_clients(first_of_year)
125
+
126
+ # and so on ...
127
+ ```
128
+
129
+ ### License
130
+
131
+ MIT License. Copyright 2013 Leopold O'Donnell
132
+
133
+
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env rake
2
+ require 'rspec/core/rake_task'
3
+ begin
4
+ require 'bundler/setup'
5
+ rescue LoadError
6
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
+ end
8
+ begin
9
+ require 'rdoc/task'
10
+ rescue LoadError
11
+ require 'rdoc/rdoc'
12
+ require 'rake/rdoctask'
13
+ RDoc::Task = Rake::RDocTask
14
+ end
15
+
16
+ RDoc::Task.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'UsesStoredProcedures'
19
+ rdoc.options << '--line-numbers'
20
+ rdoc.rdoc_files.include('README.rdoc')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
23
+
24
+ Bundler::GemHelper.install_tasks
25
+
26
+ desc "Run all specs"
27
+ RSpec::Core::RakeTask.new(:spec)
28
+
29
+ task :default => :spec
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :uses-store-procedures do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,107 @@
1
+ require 'uses_stored_procedures/hash_with_attributes'
2
+
3
+ ##
4
+ # UsesStoredProcedures provides that ability to extend a class with
5
+ # the ability to call stored_procedures providing an array of hash
6
+ # entries that can be mapped to an array of other objects.
7
+ #
8
+ module UsesStoredProcedures
9
+ extend ActiveSupport::Concern
10
+
11
+ module ClassMethods
12
+ ##
13
+ # Add a stored procedure that can ba called on a class or a
14
+ # class instance.
15
+ #
16
+ # @param [Symbol] name is the symbolic name of the method to be created.
17
+ # By default, it is assumed that the stored procedure has the same
18
+ # name as a string.
19
+ #
20
+ # @param args is a variable list of optional arguments. Available Options
21
+ # include:
22
+ #
23
+ # * :proc_name - is the name of the stored procedure. Use this if the
24
+ # name doesn't match the name paramter
25
+ # * :filter - is a class method used to filter the return results. The
26
+ # method should take a HashWithAttributes instance and return an object
27
+ # for the array entry
28
+ #
29
+ # @param [Proc] block - is a block that takes a HashWithAttributes instance
30
+ # and returns an object for the array entry.
31
+ #
32
+ # @raises if the current SQL adapter isn't supported.
33
+ #
34
+ def uses_stored_proc(name, *args, &block)
35
+ options = args.extract_options!
36
+ proc_name = options[:proc_name] || name.to_s
37
+
38
+ # Install the row mapper block or method
39
+ self.install_stored_proc_methods(name, proc_name, call_stored_proc_verb, options[:filter] || block)
40
+ end
41
+
42
+
43
+ # Return the SQL verb for calling a stored procedure.
44
+ def call_stored_proc_verb()
45
+ case ActiveRecord::Base.connection.adapter_name.to_sym
46
+ when :Mysql2, :MySQL
47
+ 'call'
48
+ when :PostgreSQL
49
+ 'select'
50
+ when :SQLServer
51
+ 'exec'
52
+ else
53
+ raise "uses_stored_procedurs does not support your connection adapter"
54
+ end
55
+ end
56
+
57
+ # Define the class and instance methods. Note the need to
58
+ # create a singleton class instance to create the stored
59
+ # procedure class method.
60
+ def install_stored_proc_methods(name, proc_name, call_stored_proc_verb, filter) #:nodoc:
61
+ define_method name do |*args|
62
+ self.class.exec_stored_proc(proc_name, call_stored_proc_verb, filter, args)
63
+ end
64
+
65
+ singleton_class = class << self; self; end
66
+ singleton_class.send(:define_method, name) do |*args|
67
+ exec_stored_proc(proc_name, call_stored_proc_verb, filter, args)
68
+ end
69
+
70
+ end
71
+
72
+ # Run the stored procedure with the arguments provided and send
73
+ # them through a filter method or block if specified.
74
+ def exec_stored_proc(name, call_stored_proc_verb, filter, args) #:nodoc:
75
+ sql = "#{call_stored_proc_verb} #{name} (" + args.map {|a| "'#{a}'"}.join(',') + ")"
76
+
77
+ # Call the stored procedure. Note the need to reset the connection
78
+ # because the mysql connection tends to hangup when stored procedures
79
+ # are called. Note that testing for ::connected? doesn't work.
80
+ #
81
+ # TODO: investigate why stored procedures cause connection hangups.
82
+ records = ActiveRecord::Base.connection.select_all(sql)
83
+ ActiveRecord::Base.connection.reconnect!
84
+
85
+ filter_stored_proc_results(records, filter)
86
+ end
87
+
88
+ private
89
+ def filter_stored_proc_results(results, filter)
90
+ case filter
91
+ when Symbol
92
+ # Map with a member function
93
+ results.map do |r| self.send(filter.to_s, HashWithAttributes.new(r)) end
94
+ when Proc
95
+ # Map with a block
96
+ results.map do |r| filter.call(HashWithAttributes.new(r)) end
97
+ else
98
+ # Just map it
99
+ results.map do |r| HashWithAttributes.new r end
100
+ end
101
+ end
102
+
103
+ end
104
+ end
105
+
106
+ require 'uses_stored_procedures/railtie'
107
+
@@ -0,0 +1,68 @@
1
+ module UsesStoredProcedures
2
+ ##
3
+ # HashWithAttributes augments Hash with getters and setters on
4
+ # an as needed basis.
5
+ #
6
+ # When ever an instance of HashWithAttributes is sent an accessor
7
+ # message, or a respond_to? message that matches a hash key for
8
+ # the first time, a setter and getter is created for that key which
9
+ # may be either a symbol or a string.
10
+ #
11
+ # @example
12
+ #
13
+ # h = HashWithAttributes.new
14
+ # h[:one] = 1
15
+ # h['two'] = 2
16
+ # h.one = 3
17
+ # h.two = 4
18
+ # puts "One is #{h.one} and Two is #{h.two}"
19
+ # # > One is 3 and Two is 4
20
+ #
21
+ class HashWithAttributes < Hash
22
+
23
+ ##
24
+ # Create a new HashWithAttributes.
25
+ #
26
+ # @param [Object] hash_or_obj will initialize the hash
27
+ # from another hash, or work like Hash where the object
28
+ # is the default for empty entries.
29
+ #
30
+ def initialize(hash_or_obj = nil)
31
+ if hash_or_obj && hash_or_obj.kind_of?(Hash)
32
+ super()
33
+ self.merge! hash_or_obj
34
+ else
35
+ super
36
+ end
37
+ end
38
+
39
+ def method_missing(name, *args, &block)
40
+ if check_and_create_accessors(name)
41
+ args.length > 0 ? send(name.to_s, args.first) : send(name.to_s)
42
+ else
43
+ super
44
+ end
45
+ end
46
+
47
+ def respond_to?(symbol, include_private = false)
48
+ super || check_and_create_accessors(symbol)
49
+ end
50
+
51
+ private
52
+ def check_and_create_accessors(name)
53
+ item_name = name.to_s.chomp '='
54
+ if has_key?(item_name) || has_key?(item_name.to_sym)
55
+ self.class.create_accessors(item_name)
56
+ return true
57
+ end
58
+ false
59
+ end
60
+
61
+ def self.create_accessors(accessor)
62
+ define_method(accessor) { fetch accessor }
63
+ define_method("#{accessor}=") { |v| store(accessor, v) }
64
+ true
65
+ end
66
+ end
67
+
68
+ end
@@ -0,0 +1,10 @@
1
+ require 'active_record/railtie'
2
+ require 'active_support/core_ext'
3
+
4
+ module UsesStoredProcedures
5
+ class Railtie < Rails::Railtie
6
+ if defined?(ActiveRecord::Base)
7
+ ActiveRecord::Base.send :include, UsesStoredProcedures
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ module UsesStoreProcedures
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: uses-store-procedures
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Leo O'Donnell
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-03-01 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 3.2.12
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 3.2.12
30
+ description: ! 'So, you really need to use stored procedures in your Rails application.
31
+ This gem extends
32
+
33
+ ActiveRecord with a class method to add to your models or services.
34
+
35
+ '
36
+ email:
37
+ - leopold.odonnell@gmail.com
38
+ executables: []
39
+ extensions: []
40
+ extra_rdoc_files: []
41
+ files:
42
+ - lib/tasks/uses-stored-procedures_tasks.rake
43
+ - lib/uses_stored_procedures/hash_with_attributes.rb
44
+ - lib/uses_stored_procedures/railtie.rb
45
+ - lib/uses_stored_procedures/version.rb
46
+ - lib/uses_stored_procedures.rb
47
+ - MIT-LICENSE
48
+ - Rakefile
49
+ - README.md
50
+ homepage: https://github.com/leopoldodonnell/uses-stored-procedures
51
+ licenses: []
52
+ post_install_message:
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ! '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ requirements: []
69
+ rubyforge_project:
70
+ rubygems_version: 1.8.24
71
+ signing_key:
72
+ specification_version: 3
73
+ summary: Extend ActiveRecord with the ability to use Stored Procedures.
74
+ test_files: []
75
+ has_rdoc: