Rails下cloud datastore的使用

 


 Rails下cloud datastore的使用

背景

部门有一个项目要用Ruby做 WebAPI,DB使用关系型数据库Cloud Sql和非关系型数据库Cloud Datastore 。

还不了解Ruby On Rails和CloudDatastore的请参考下面的链接。

http://guides.ruby-china.org/

https://thinkit.co.jp/story/2015/02/05/5594

 

1、 Windows下开发环境构建

1.1、构建Ruby On Rails 开发环境

① Ruby安装

http://www.runoob.com/ruby/ruby-installation-windows.html

② Rail安装

从CMD提示窗口输入指令:gem install rails 开始安装rails。

注意:在中国直接安装会提示错误,因为gem默认的安装源https://rubygems.org在国内不能访问,还好先辈们早已为我们新搭建了一个gem安装源http://gems.ruby-china.org。下面是切换安装源的具体步骤,gem安装源切换之后,Rails安装就可以顺利进行了。

第一步:删除默认安装源

CMD提示窗中输入”gem sources -remove https://rubygems.org/”,回车换行。

第二步:添加新的安装源

CMD提示窗输入“gem sources -add  http://gems.ruby-china.org/”,回车换行。

③ ruby的IDE Ruby Mine安装

http://qiita.com/HALU5071/items/6d6a39e44865d8d04de8

1.2、搭建开发用Cloud Datastore 数据库

① 安装Google Cloud SDK

http://www.apps-gcp.com/google-cloud-sdk-install

② 安装cloud-datastore-emulator

CMD提示窗中输入“gcloud components install cloud-datastore-emulator”,回车换行。

③ 创建一个开发用的DB:dev_datastore

CMD提示窗中输入“cloud_datastore_emulator create db/dev_datastore”,回车换行。

前提条件:CMD当前运行目录下要先创建一个db的文件夹。

创建好的DB文件结构图如下:

 

2、 WEB API工程创建

在CMD提示窗中输入“rails new my-first-api --api”,回车换行。

创建好的API工程结构见下图,rails执行结果见附录1。

フォルダ構造

 

 

説明

Gemfile

 

gemの依存関係を指定できるファイル

README.rdoc

 

説明書

Rakefile

ターミナルから実行可能なタスク

 

config.ru

 

Rackの設定

app/

 

アプリケーションを格納するディレクトリ

主要なプログラムはこの配下に格納

 

controllers/

コントローラを格納するディレクトリ

 

controllers/application_controller.rb

アプリケーションで共通のコントローラ

 

models/

モデルを格納するディレクトリ

config/

 

プロジェクトの設定ファイルを格納するディレクトリ

config/environments/

 

環境単位の設定ファイルを格納するディレクトリ

config/initializers/

 

初期化ファイルを格納するディレクトリ

config/locales/

 

辞書ファイルを格納するディレクトリ

db/

 

データベースの設定ファイルを格納するディレクトリ

doc/

 

ドキュメントを格納するディレクトリ

lib/

 

複数のアプリケーション間で共有するライブラリを格納するディレクトリ

 

tasks/

自分で生成したRakefileを格納するディレクトリ

log/

 

ログファイルが格納されるディレクトリ。ログファイルはアプリケーションと環境ごとに作成される

public/

 

Web上に公開するファイルを格納するディレクトリ

tmp/

 

キャッシュなど、一時的なファイルを格納されるディレクトリ

test/

 

アプリケーションのテストに使うファイルを格納するディレクトリ

vendor/

 

ライブラリや他のアプリケーションで共有するような外部ライブラリを格納するディレクトリ

 

在附录1的最后我们看到,bundle install 并没有成功。原因是项目的gem安装源出了问题。打开Gemfile把第一行的source 'https://rubygems.org'替换成source 'http://gems.ruby-china.org'。然后在CMD里重新执行bundle install命令,这样项目需要的gem就能成功安装。

 

3、 新建cloud Datastore 的dataset对象

① Gem安装

在Gemfile 中追加

gem 'google-cloud-datastore'

cmd里重新执行bundle install命令。

② 配置文件

在config/initializers/目录下新建cloud_datastore.rb文件,文件内容如下图所示。

从图中我们可以看到cloud datastore 配置了三种环境下创建daset的参数,这些参数我们都放在了config/database.yml里。下面是database.yml里开发环境的配置信息,datastore 安装的主机和datastore的数据库名,这样我们就可以使用datastore了。

development:
 
        host: 'localhost:8180'
 
        project: ' dev_datastore '

 

 

cloud_datastore.rb

 

4、 非关系型数据库 中ActiveModel 的使用

如果我们想要cloud datastore和关系型数据库一样,可以方便快捷使用model(ActiveModel),怎么办?下面就是你想要的答案。

① 追加文件

在config/initializers/目录下新建active_model_cloud_datastore.rb文件。

文件内容参照附录2。

② model Class写法

require_relative "../../config/initializers/active_model_cloud_datastore" 
class Customer
include ActiveModelCloudDatastore
attr_accessor :customer_code, :customer_name

def attributes
 
%w(code customer_name)# 注意多个字段之间是半角空格来区分的
end
end

这样我们就可以使用activeModel很多功能,

#检索所有数据

@customers = Customer.all

#保存一条新的数据

@customer = Customer.new

@customer.save

这里就不做详细说明,具体参照附录2里的各种方法。

5、 spec测试

  1. 1.   
  2. 2.   
  3. 3.   
  4. 4.   
  5. 5.   

① 测试环境安装

在Gemfile中追加下面的gem。

group :development, :test do 
 
# Test
 
gem 'rspec-rails', '~> 3.0'
 
gem 'rails-controller-testing'
end

然后在cmd工程目录my-first-api下执行bundle install,安装追加的gem。

② 测试环境初期化

在cmd工程目录my-first-api下执行

bundle exec rails generate rspec:install

会生成下面的文件

create  .rspec
create  spec
create  spec/spec_helper.rb
create  spec/rails_helper.rb

③ controller测试

参考下面链接

http://qiita.com/shizuma/items/84e07e558abd6593df15

http://blog.naichilab.com/entry/2016/01/19/011514

④ model测试

参考下面的测试思路。

http://qiita.com/shizuma/items/c7b8d7b91e8f325f8ad9

⑤ Mock使用

参考下面链接

http://qiita.com/jnchito/items/640f17e124ab263a54dd

6、 测试覆盖率报告

①  环境搭建

在Gemfile中追加下面的gem。

group :development, :test do
 
gem 'simplecov'
end

然后在cmd工程目录my-first-api下执行bundle install,安装追加的gem。

② spec_helper.rb修改

在spec_helper.rb头部插入下面的语句,覆盖率测试中要过滤掉spec下的测试代码。

require 'simplecov'
SimpleCov.start do
 
add_filter "/spec/ "
end

③ 查看报告

在cmd工程目录my-first-api下,再次执行rspec spec命令,覆盖率报告就会自动生成再demo/coverage下,用google chrome浏览器打开index.html,就可以看到详细的信息。下面是一个覆盖率报告的截图。


7、 结束语

上面我们讲述的是Ruby下怎么使用cloud datastore的开发和测试,在Google Cloud Platform上怎么部署产品还有待下一步探索。期间遇到的各种技术问题难题,为了解决这些问题,调查的网站以日语和英语为主,总结的时候也使用了很多日语网站,由于时间有限,没能一一翻译过来,给不懂日语的朋友带来不少困难表示歉意。


附录1

API工程创建文件list

create

create  README.md

create  Rakefile

create  config.ru

create  .gitignore

create  Gemfile

create  app

create  app/assets/config/manifest.js

create  app/assets/javascripts/application.js

create  app/assets/javascripts/cable.js

create  app/assets/stylesheets/application.css

create  app/channels/application_cable/channel.rb

create  app/channels/application_cable/connection.rb

create  app/controllers/application_controller.rb

create  app/helpers/application_helper.rb

create  app/jobs/application_job.rb

create  app/mailers/application_mailer.rb

create  app/models/application_record.rb

create  app/views/layouts/application.html.erb

create  app/views/layouts/mailer.html.erb

create  app/views/layouts/mailer.text.erb

create  app/assets/images/.keep

create  app/assets/javascripts/channels

create  app/assets/javascripts/channels/.keep

create  app/controllers/concerns/.keep

create  app/models/concerns/.keep

create  bin

create  bin/bundle

create  bin/rails

create  bin/rake

create  bin/setup

create  bin/update

create  config

create  config/routes.rb

create  config/application.rb

create  config/environment.rb

create  config/secrets.yml

create  config/cable.yml

create  config/puma.rb

create  config/environments

create  config/environments/development.rb

create  config/environments/production.rb

create  config/environments/test.rb

create  config/initializers

create  config/initializers/application_controller_renderer.rb

create  config/initializers/assets.rb

create  config/initializers/backtrace_silencers.rb

create  config/initializers/cookies_serializer.rb

create  config/initializers/cors.rb

create  config/initializers/filter_parameter_logging.rb

create  config/initializers/inflections.rb

create  config/initializers/mime_types.rb

create  config/initializers/new_framework_defaults.rb

create  config/initializers/session_store.rb

create  config/initializers/wrap_parameters.rb

create  config/locales

create  config/locales/en.yml

create  config/boot.rb

create  config/database.yml

create  db

create  db/seeds.rb

create  lib

create  lib/tasks

create  lib/tasks/.keep

create  lib/assets

create  lib/assets/.keep

create  log

create  log/.keep

create  public

create  public/404.html

create  public/422.html

create  public/500.html

create  public/apple-touch-icon-precomposed.png

create  public/apple-touch-icon.png

create  public/favicon.ico

create  public/robots.txt

create  test/fixtures

create  test/fixtures/.keep

create  test/fixtures/files

create  test/fixtures/files/.keep

create  test/controllers

create  test/controllers/.keep

create  test/mailers

create  test/mailers/.keep

create  test/models

create  test/models/.keep

create  test/helpers

create  test/helpers/.keep

create  test/integration

create  test/integration/.keep

create  test/test_helper.rb

create  tmp

create  tmp/.keep

create  tmp/cache

create  tmp/cache/assets

create  vendor/assets/stylesheets

create  vendor/assets/stylesheets/.keep

remove  app/assets

remove  lib/assets

remove  tmp/cache/assets

remove  vendor/assets

remove  app/helpers

remove  test/helpers

remove  app/views/layouts/application.html.erb

remove  public/404.html

remove  public/422.html

remove  public/500.html

remove  public/apple-touch-icon-precomposed.png

remove  public/apple-touch-icon.png

remove  public/favicon.ico

remove  app/assets/javascripts

remove  config/initializers/assets.rb

remove  config/initializers/session_store.rb

remove  config/initializers/cookies_serializer.rb

Fetching gem metadata from https://rubygems.org/..........

Fetching version metadata from https://rubygems.org/..

Fetching dependency metadata from https://rubygems.org/.

Resolving dependencies...

Using rake 12.0.0

Using i18n 0.7.0

Using minitest 5.10.1

Using thread_safe 0.3.5

Using builder 3.2.2

Using erubis 2.7.0

Using mini_portile2 2.1.0

Using rack 2.0.1

Using nio4r 1.2.1

Using websocket-extensions 0.1.2

Using mime-types-data 3.2016.0521

Using arel 7.1.4

Using bundler 1.13.6

Using method_source 0.8.2

Using puma 3.6.2

Using thor 0.19.4

Using sqlite3 1.3.12

Gem::RemoteFetcher::FetchError: SSL_connect returned=1 errno=0 state=SSLv3 read

server certificate B: certificate verify failed

(https://rubygems.org/gems/concurrent-ruby-1.0.4.gem)

An error occurred while installing concurrent-ruby (1.0.4), and Bundler cannot

continue.

Make sure that `gem install concurrent-ruby -v '1.0.4'` succeeds before

bundling.


附录2

active_model_cloud_datastore.rb文件内容:

# frozen_string_literal: true

require_relative 'cloud_datastore'
require 'active_model'
require 'active_support'


# Integrates ActiveModel with the Google::Cloud::Datastore
module ActiveModelCloudDatastore
 
extend ActiveSupport::Concern
 
include ActiveModel::Model
 
include ActiveModel::Dirty
 
include ActiveModel::Validations
 
include ActiveModel::Validations::Callbacks

 
included do
   
private_class_method :query_options, :query_sort, :query_property_filter
   
define_model_callbacks :save, :update, :destroy
   
attr_accessor :id
 
end

  def
attributes
   
[]
  end

 
# Used by ActiveModel for determining polymorphic routing.
 
def persisted?
   
id.present?
  end

 
# Resets the ActiveModel::Dirty tracked changes
 
def reload!
   
clear_changes_information
  end

 
# Updates attribute values on the ActiveModel::Model object with the provided params.
  # Example, such as submitted form params.
  #
  # @param [Hash] params
 
def update_model_attributes(params)
    params.each do |name, value|
      send "#{name}=", value if respond_to? "#{name}="
   
end
  end

 
# Builds the Cloud Datastore entity with attributes from the Model object.
  #
  # @return [Entity] the updated Google::Cloud::Datastore::Entity
 
def build_entity(parent = nil)
    entity = CloudDatastore.dataset.entity(self.class.name, id)
    entity.key.parent = parent if parent
   
attributes.each do |attr|
      entity[attr] = instance_variable_get("@#{attr}")
    end
   
entity
 
end

  def
save(parent = nil)
    run_callbacks :save do
      if
valid?
        entity = build_entity(parent)
        success = self.class.retry_on_exception? { CloudDatastore.dataset.save(entity) }
        if success
         
self.id = entity.key.id
          return true
        end
      end
      false
    end
  end

  def
update(params)
    run_callbacks :update do
     
update_model_attributes(params)
      if valid?
        entity = build_entity
        self.class.retry_on_exception? { CloudDatastore.dataset.save(entity) }
      else
        false
      end
    end
  end

  def
destroy
   
run_callbacks :destroy do
     
key = CloudDatastore.dataset.key(self.class.name, id)
      self.class.retry_on_exception? { CloudDatastore.dataset.delete(key) }
    end
  end

 
# Methods defined here will be class methods whenever we 'include DatastoreUtils'.
 
module ClassMethods
   
# Queries all objects from Cloud Datastore by named kind and using the provided options.
    #
    # @param [Hash] options the options to construct the query with.
    #
    # @option options [Google::Cloud::Datastore::Key] :ancestor filter for inherited results
    # @option options [Hash] :where filter, Array in the format [name, operator, value]
    #
    # @return [Array<Model>] an array of ActiveModel results.
   
def all(options = {})
      query = CloudDatastore.dataset.query(name)
      query.ancestor(options[:ancestor]) if options[:ancestor]
      query_property_filter(query, options)
      entities = retry_on_exception { CloudDatastore.dataset.run(query) }
      from_entities(entities.flatten)
    end

   
# Queries objects from Cloud Datastore in batches by named kind and using the provided options.
    # When a limit option is provided queries up to the limit and returns results with a cursor.
    #
    # @param [Hash] options the options to construct the query with. See build_query for options.
    #
    # @return [Array<Model>, String] an array of ActiveModel results and a cursor that can be used
    # to query for additional results.
   
def find_in_batches(options = {})
      next_cursor = nil
     
query = build_query(options)
      if options[:limit]
        entities = retry_on_exception { CloudDatastore.dataset.run(query) }
        next_cursor = entities.cursor if entities.size == options[:limit]
      else
       
entities = retry_on_exception { CloudDatastore.dataset.run(query) }
      end
     
model_entities = from_entities(entities.flatten)
      return model_entities, next_cursor
   
end

   
# Retrieves an entity by key and by an optional parent.
    #
    # @param [Integer or String] id_or_name id or name value of the entity Key.
    # @param [Google::Cloud::Datastore::Key] parent the parent Key of the entity.
    #
    # @return [Entity, nil] a Google::Cloud::Datastore::Entity object or nil.
   
def find_entity(id_or_name, parent = nil)
      key = CloudDatastore.dataset.key(name, id_or_name)
      key.parent = parent if parent
     
retry_on_exception { CloudDatastore.dataset.find(key) }
    end

   
# Find object by ID.
    #
    # @return [Model, nil] an ActiveModel object or nil.
   
def find(id)
      entity = find_entity(id.to_i)
      from_entity(entity)
    end

    
# Find object by parent and ID.
    #
    # @return [Model, nil] an ActiveModel object or nil.
   
def find_by_parent(id, parent)
      entity = find_entity(id.to_i, parent)
      from_entity(entity)
    end

    def
from_entities(entities)
      entities.map { |entity| from_entity(entity) }
    end

   
# Translates between Google::Cloud::Datastore::Entity objects and ActiveModel::Model objects.
    #
    # @param [Entity] entity from Cloud Datastore
    # @return [Model] the translated ActiveModel object.
   
def from_entity(entity)
      return if entity.nil?
      model_entity = new
      model_entity.id = entity.key.id unless entity.key.id.nil?
      model_entity.id = entity.key.name unless entity.key.name.nil?
      entity.properties.to_hash.each do |name, value|
        model_entity.send "#{name}=", value
     
end
     
model_entity.reload!
      model_entity
   
end

    def
exclude_from_index(entity, boolean)
      entity.properties.to_h.keys.each do |value|
        entity.exclude_from_indexes! value, boolean
     
end
    end

   
# Constructs a Google::Cloud::Datastore::Query.
    #
    # @param [Hash] options the options to construct the query with.
    #
    # @option options [Google::Cloud::Datastore::Key] :ancestor filter for inherited results
    # @option options [String] :cursor sets the cursor to start the results at
    # @option options [Integer] :limit sets a limit to the number of results to be returned
    # @option options [String] :order sort the results by property name
    # @option options [String] :desc_order sort the results by descending property name
    # @option options [Array] :select retrieve only select properties from the matched entities
    # @option options [Hash] :where filter, Array in the format [name, operator, value]
    #
    # @return [Query] a datastore query.
   
def build_query(options = {})
      query = CloudDatastore.dataset.query(name)
      query_options(query, options)
    end

    def
retry_on_exception?
     
retry_count = 0
      sleep_time = 0.5 # 0.5, 1, 2, 4 second between retries
     
begin
        yield
      rescue
=> e
       
puts "\e[33m[#{e.message.inspect}]\e[0m"
       
puts 'Rescued exception, retrying...'
       
sleep sleep_time
        sleep_time
*= 2
        retry_count += 1
        return false if retry_count > 3
        retry
      end
      true
    end

    def
retry_on_exception
     
retry_count = 0
      sleep_time = 0.5 # 0.5, 1, 2, 4 second between retries
     
begin
        yield
      rescue
=> e
       
puts "\e[33m[#{e.message.inspect}]\e[0m"
       
puts 'Rescued exception, retrying...'
       
sleep sleep_time
        sleep_time
*= 2
        retry_count += 1
        raise e if retry_count > 3
        retry
      end
    end

    def
log_google_cloud_error
     
yield
    rescue
Google::Cloud::Error => e
     
puts "\e[33m[#{e.message.inspect}]\e[0m"
     
raise e
   
end

   
# private

   
def query_options(query, options)
      query.ancestor(options[:ancestor]) if options[:ancestor]
      query.cursor(options[:cursor]) if options[:cursor]
      query.limit(options[:limit]) if options[:limit]
      query_sort(query, options)
      query.select(options[:select]) if options[:select]
      query_property_filter(query, options)
    end

   
# Adds sorting to the results by a property name if included in the options.
   
def query_sort(query, options)
      query.order(options[:order]) if options[:order]
      query.order(options[:desc_order], :desc) if options[:desc_order]
      query
   
end

   
# Adds property filters to the query if included in the options.
    # Accepts individual or nested Arrays:
    # [['superseded', '=', false], ['email', '=', 'something']]
   
def query_property_filter(query, options)
      if options[:where]
        opts = options[:where]
        if opts[0].is_a?(Array)
          opts.each do |opt|
            query.where(opt[0], opt[1], opt[2]) unless opt.nil?
          end
        else
         
query.where(opts[0], opts[1], opts[2])
        end
      end
     
query
   
end
  end
end

 

posted on 2016-12-30 17:42  蜗牛漫步IT  阅读(525)  评论(0编辑  收藏  举报

导航