a guest Jun 15th, 2017 55 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. # Ruby Gem development and ActiveRecord (without Rails)
  3. Recently I was writing a gem that needed a database. I didn't need the full functionality of Rails but wanted tooling and some structure to my code. ActiveRecord was my obvious choice for database interactions and PostgreSQL was my personal choice for the database. There are a few blog posts floating around that go over using AR without Rails but still left me with issues un-anwsered. AR documentation wasn't terribly helpful without diving deeper because of how tightly it's integrated with Rails. This post hopefully will be a little more thorough and cover the issues I ran into.  
  5. ### Installing dependencies
  7. First lets include AR and Postgres. No-brainer.  
  9. Including AR and dependencies can be done through the usual means. We have two ways to include dependencies `add_development_dependency` and `add_runtime_dependency`. While developing the gem running `gem install --dev gem_name` will include both development and runtime dependencies. Development dependencies usually include libraries for testing and debugging.
  11. ```ruby
  12. # gem_name.gemspec
  13. do |spec|
  14.     ...
  15.     spec.add_runtime_dependency 'active_record', '~> 5.1'
  16.     spec.add_runtime_dependency 'pg', '~> 0.20.0'
  17. end
  18. ```
  20. You can add or leave off semantic-versioning. We've included versioning to make sure we have the most up-to-date libraries. Run `bundle install` from the command-line. Now AR and Postgres should be available to us.
  22. ### A better understanding of Ruby's Load Path
  24. In this post we are integrating AR into a gem but you might have a different use case. File structure is something to be aware of regardless. I quickly ran into errors because of my file structure and not requiring the files correctly. Lets quickly go through ruby and requiring files.
  26. The `$LOAD_PATH` is a global variable that's an array of absolute paths. My `$LOAD_PATH` is below. When you package your gem, RubyGems will add your gem's `lib/` directory to the `$LOAD_PATH`. If you take careful notice to my `$LOAD_PATH` we can see a couple of things. The initial `$LOAD_PATH` contains paths for Ruby's Standard Library. Second thing we can see is the AR and Postgres gem's we installed have thier `lib/` directory exposed in the `$LOAD_PATH`. You should see your own gem's `lib/` directory added as well.
  28. ```
  29. [1]pry(main)> $LOAD_PATH
  30. => ["/home/cmckee/Development/Ruby/gem_name/lib",
  31. ...
  32.  "/home/cmckee/.rvm/gems/ruby-2.4.0/gems/pg-0.20.0/lib",
  33. ...
  34.  "/home/cmckee/.rvm/gems/ruby-2.4.0/gems/activerecord-5.1.1/lib",
  35. ...
  36.  "/home/cmckee/.rvm/rubies/ruby-2.4.0/lib/ruby/2.4.0",
  37.  "/home/cmckee/.rvm/rubies/ruby-2.4.0/lib/ruby/2.4.0/x86_64-linux"]
  38. ```
  40. While RubyGems loading our `lib/` directory to the `$LOAD_PATH` for easy requiring of files is very beneficial, if we aren't careful this could comeback and bite us. We need to be aware of what files we place in the top level of our `lib/` directory. They become directly-requirable and this could possibly cause issues. It's good practice to namespace our files under another directory. Similar to below.
  42. ```
  43. ▾ lib/
  44.   ▾ config/
  45.       database.yml
  46.       application.rb
  47.   ▾ db/
  48.     ▸ migrations/
  49.       schema.rb
  50.       seed.rb
  51.   ▸ gem_name/
  52.   ▾ models/
  53.         device.rb
  54. ```
  56. Lastly, both `load` and `require` are ruby kernel methods. When calling them, ruby searches through files in the `$LOAD_PATH`. This allows us to require files relative to the `$LOAD_PATH` instead of specifing the entire path. So what's the difference between using `load` vs `require`? There is a small difference between the two but I think it's a useful one to know. The `load` method loads the file everytime you call it and `require` will only work on the first call.
  58. > You use load() to execute code, and you use require() to import libraries. - Metaprogramming Ruby
  60. ### Setting up configuration files
  62. Now that we have a better undertanding lets create a `config/` folder under the `lib/` directory to hold our configuration files. We will be setting up two files `config/application.rb` and `config/database.yml`.
  64. We will want a place for configuration, initialization and other functionality that is available to the entire gem. Depending on how large your codebase becomes you might want to separate configuration and initialization, but for our gem putting that into `application.rb` should be just fine. We require gems, define a few constant variables, establish AR connection and recursively require all models.
  66. ```ruby
  67. # config/application.rb
  68. require 'active_record'
  69. require 'yaml'
  71. DATABASE_ENV = ENV['RACK_ENV'] || 'development'
  72. MIGRATION_DIR = ENV['MIGRATION_DIR'] || 'lib/db/migrations/'
  73. DATABASE_CONFIG = YAML.load_file('lib/config/database.yml')
  75. ActiveRecord::Base.establish_connection(DATABSE_CONFIG[DATABASE_ENV])
  77. Dir.glob('lib/models/*.rb').each { |f| require f }
  78. ```
  80. For the database configuration I opted to use a YAML file. It's easy to read and changes only need to be made in a single place. It also mimics what is used in Rails so you might be familiar with it. We will be using Postgres for this project so remember to set the adapter to postgres. I ran into a few errors AR threw at me for the adapter not being set or called correctly so keep this in mind.
  82. ```
  83. # config/database.yml
  85. default: &default
  86.     adapter: postgresql
  87.     encoding: unicode
  88.     username: [db_username_goes_here]
  89.     password: [db_password_goes_here]
  90.     host: localhost
  91.     port: 5432
  92.     timeout: 5000
  93.     pool: 5
  95. development:
  96.     <<: &default
  97.     database: gem_name_development
  99. production:
  100.     <<: &default
  101.     database: gem_name_production
  102. ```
  104. ### Creating some useful Rake Tasks
  106. If you haven't writen a rake task before its pretty simple. We will be writing a few rake tasks that interact with AR and re-create some rails functionality that will make our development process quicker and easier. The first piece of a rake task is requiring the libraries. We will require the application configuration file for this functionality. The second part is being familiar with Rake's `namespace` and being aware of the scope that it creates.
  108. `rake db:create`
  110. ```ruby
  111. # Rakefile
  112. require 'config/application.rb'
  113. ...
  114. namespace :db do
  115. ...
  116.     desc "Create database"
  117.     task :create do
  118.         database =[DATABASE_ENV])
  119.         database.create
  120.     end
  121. end
  122. ```
  124. `rake db:migrate`
  126. ```ruby
  127. # Rakefile
  128. ...
  129. namespace :db do
  130. ...
  131. desc "Migrate database"
  132.     task :migrate  => :configure_connection do
  133.         ActiveRecord::Migration.verbose = true
  134.         ActiveRecord::Migrator.migrate(MIGRATION_DIR, ENV['VERSION'] ? ENV['VERSION'].to_i : nil)
  135."lib/db/schema.rb", "w:utf-8") do |file|
  136.           ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
  137.         end
  138.     end
  139. end
  140. ```
  142. `rake db:drop`
  144. ```ruby
  145. # Rakefile
  146. ...
  147. namespace :db do
  148. ...
  149.     desc "Drop databse"
  150.     task :drop  => :configure_connection do
  151.         database =[DATABASE_ENV])
  152.         database.drop
  153.     end
  154. end
  155. ```
  157. `rake db:reset`
  159. ```ruby
  160. # Rakefile
  161. ...
  162. namespace :db do
  163. ...
  164.     task :reset => [:drop, :create, :migrate]
  165. end
  166. ```
  168. These four rake tasks are common in interacting with the database.
  170. We will add two more rake tasks that I think will add to the development process with AR. The first one will help generate migration files. We will create a new namespace for this rake task. `:g`, short for generate. Again following familiar conventions. The rake task creates a file in the correct directory and generates boilerplate content in the file. Once the boiler plate is generated you will have to go and edit the file with your secific migrations.
  172. `rake g:migration [FILE_NAME]`
  174. ```ruby
  175. # Rakefile
  176. ...
  177. namespace :g do
  178.    desc "Generate Migration"
  179.    task :migration do
  180.      name = ARGV[1] || raise("Specify name: rake g:migration your_migration")
  181.      timestamp ="%Y%m%d%H%M%S")
  182.      path = File.expand_path("lib/db/migrations/#{timestamp}_#{name}.rb", __FILE__)
  183.      migration_class = name.split("_").map(&:capitalize).join
  185., "w") do |file|
  186.        file.write <<-EOF
  187. class #{migration_class} < ActiveRecord::Migration[5.1]
  188.   def self.up
  189.   end
  190.   def self.down
  191.   end
  192. end
  193.        EOF
  194.      end
  196.      puts "Migration #{path} created"
  197.      abort # needed stop other tasks
  198.   end
  199. end
  200. ```
  202. I came across an issue with newer version's of AR where the migration class needs to inherit from AR but AR now requires a version to be specified. `class CreateDevice < ActiveRecord::Migration[5.1]`. In this rake task I manually specified the version I am using. Although you can update this rake task to be more dynamic.
  204. The last rake task we will write I find myself using frequently. This rake task doesnt need a namespace. It will connect with the database and enter a REPL session. My personal preference is to use `pry` so we will require pry, establish a connection with the database and then start pry.
  207. `rake console`
  209. ```ruby
  210. require 'config/application.rb'
  212. task :console do
  213.   require 'pry'
  214.   ActiveRecord::Base.connection
  215.   Pry.start
  216. end
  217. ```
  219. ### Impliment ActiveRecord logging  
  221. If you're familiar with Rails, you've undoubtedly used the `rails console`. Infact we just went over writing a basic rake task for this. Running AR commands like `Device.first` or `Device.find_by_id(1)` while inside the console would normally print the actual SQL statements that were queried back. By default our implimentation has no logging like this.
  223. ```ruby
  224. # config/applcation.rb
  225. ...
  226. ActiveRecord::Base.logger = STDOUT
  227. ```
  229. It is a pretty simple fix. `ActiveRecord::Base.logging` is the AR functionality we are missing that logs the SQL queries, timestamps, etc. We will use Ruby's Stdlib `Logger` class for the output. Now our console should look something similar to the example below. Alternatively you can set it to log to a file with `'path_to/debug.log')`.
  231. ```bash
  232. [1]pry(main)> Device.first
  233. D, [2017-06-07T00:51:45.083723 #23259] DEBUG -- :   Device Load (0.7ms)  SELECT  "devices".* FROM "devices" ORDER BY "devices"."id" ASC LIMIT $1  [["LIMIT", 1]]
  234. ```  
  236. ### A bit of ActiveRecord knowledge
  238. AR makes use of the `establish_connection` method to connect with the database. It takes a hash where the `:adapter` key must be given. Like I previously mentioned you might run into errors: `AdapterNotSpecified` or `AdapterNotFound`.
  240. We will just use the database.yml file. This helps with clarity and the database configuration is located in a single place instead of anywhere `establish_connection` might need to be called in your codebase. Depending on the format of your `database.yml` file you can also pass the environment like shown below.
  242. ```ruby
  243. ActiveRecord::Base.establish_connection(DATABSE_CONFIG[DATABASE_ENV])
  244. ```
  246. The `establish_connection` method is a little misleading. While the name implies the connection is established, it's not entirely true. It essentially sets up the configuration and patiently waits until a "real" call to the database happens. A call like creating, migrating, droping, querying, etc. Using `ActiveRecord::Base.connected?` allows us to see if the connection has truly been made. Instead of querying the database in someway to establish this connection we can use `ActiveRecord::Base.connection` to "ping" the database and create it.
RAW Paste Data
We use cookies for various purposes including analytics. By continuing to use Pastebin, you agree to our use of cookies as described in the Cookies Policy. OK, I Understand