65.9K
CodeProject 正在变化。 阅读更多。
Home

第 1 天:Spider 数据库导航器网站

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (7投票s)

2013 年 11 月 3 日

CPOL

16分钟阅读

viewsIcon

30367

downloadIcon

287

使用 Ruby on Rails 创建一个网站,用于动态显示和导航 SQL Server 数据库。

(点击此处查看全屏图像)

最新版本的代码可以在我的 GitHub 账户 此处 获取。

最初...

“在创造之初,当 Marc 建立网站时,网站是混沌而空虚的,黑暗笼罩着 HTML 的表面,一个强大的脚本掠过 IDE 的表面。Marc 说:‘要有数据库爬虫 UI’,于是就有了数据库爬虫 UI;Marc 看见 UI 是好的,他就将主键与外键分开。他称外键为父/子关系,黑暗为 Ruby on Rails。于是有了晚上,有了白天,这是第一天。” 

我一直想创建一个数据库导航器,也就是我的朋友称之为“爬虫 UI”的东西,已经很久了。我最初用 WinForms 为 Oracle 数据库写了一个实现,但它并没有像我预期的那样发展。目前,我大量使用 Ruby on Rails,并且在另一个项目中遇到了两个层,分别称为 Slim(“轻量级模板引擎”)和 Sass(“句法上很棒的样式表”)。我想更多地了解 Slim 和 Sass 是如何协同工作的,于是就有了这个项目想法。在其完整形态中,我想添加各种有趣的功能,例如用于编辑的自定义布局,但首先,我想先实现一些基本功能。

内部,网站必须支持

  • 连接到 SQL Server(是的,Ruby on Rails 可以连接到 SQL Server。)
  • 使用一个单独的数据库来存储会话和其他元数据,而保留我们连接的数据库用于导航,不受影响。
  • 不为每个表创建物理模型,而是实现一个动态模型。
  • 通过检查数据库架构来动态发现用户表和外键关系,而不是依赖于 Rails 应用程序架构(这会由基于物理表的具体模型生成)。

用户界面必须支持

  • 从列表中选择一个表
  • 查看该表的数据
  • 选择父表和子表进行导航
  • 选择记录来限定导航和显示父/子记录
  • 分页
  • 父/子导航的面包屑路径

我留给“第二天”的是几个元数据功能

  • 用“显示字段”查找替换外键 ID。
  • 在查找表中描述要用于解析外键的显示字段。
  • 指定不需要显示的字段。
  • 字段名别名
  • 表名别名

“第三天”将支持

  • 基于类型的 UI 自动生成,用于编辑表数据。
  • 自定义记录编辑模板。

“第四天”将包括

  • 视图(在元数据中描述,而不是在数据库端视图),因为需要创建具有完整架构信息的视图。
  • 创建 SQL 语句以支持视图上的事务

在此之后,可能会涉及使用 Ruby 对事务(代码和 PL/SQL 调用)进行自定义处理的支持,以及事务上的服务器端触发器。我们拭目以待!

如果您是 Ruby on Rails 新手

Code Project 目前(截至撰写本文时!)主要是一个关于微软所有事物的网站,所以如果您是第一次接触 Ruby on Rails,我建议您阅读我关于 Ruby 语言和 Ruby on Rails 的其他几篇文章

因为这些文章更侧重于为开发 Ruby on Rails (RoR) 应用程序提供教程。本文假定您已经对 RoR 应用程序的项目结构有所了解。

AdventureWorks2008

本文使用微软的示例数据库 Adventure Works 来演示 Ruby on Rails 与 SQL Server 的连接,并提供本文所有屏幕截图的示例数据集。

关于 Ruby 代码

您会注意到我倾向于编写非常短小的 Ruby 函数——这是为了提高高级函数的清晰度,因为嵌入惯用的 Ruby 和晦涩的操作可能会分散函数的目的。我也喜欢明确说明函数的返回值,即使它是不必要的。这可以进一步提高不熟悉应用程序代码的人的理解。

关于 Slim 和 Sass 标记

我尽量保持标记简洁,并在我认为合适的地方添加注释来描述标记的意图。这在 Sass 标记中尤为突出,其样式的意图并非显而易见。

Gems 及其用途

Gems 是 Ruby on Rails 的插件机制,用于添加来自人们多年来贡献的庞大免费开源组件的功能。除了 Rails gems,我利用的是

gem 'tiny_tds'
gem 'activerecord-sqlserver-adapter'
gem 'sqlite3'
gem 'slim'
gem 'thin'
gem 'sass'
gem 'will_paginate'

TinyTDS

连接到 SQL Server 所需的 tiny_tds gem。TinyTDS 需要 SQL Server 身份验证登录,而不是 Windows 身份验证登录。我在 这里 有一篇关于从 Ruby 连接到 SQL Server Express 的短博文,其中介绍了配置 SQL Server Express 和测试 TinyTDS 连接。在此应用程序中,TinyTDS 用于从 SQL Server 获取架构信息——请参阅下面的“Schema Class”部分。

activerecord-sqlserver-adapter

这个 gem 使用“dblib”连接模式,而“dblib”模式又依赖于 TinyTDS。这个 gem 使您能够使用 Rails 的 ActiveRecord API 进行所有数据库事务。重要的是我们要使用 ActiveRecord 进行表查询,因为分页系统依赖于我们的 Model 类派生自 ActiveRecord::Base。在未来的文章中,我们也将部分依赖 ActiveRecord 进行对表记录的其他事务。

Rails 期望在 config\database.yml 文件中指定连接信息。在这里,我们设置了开发连接,指定了上面 gem 提供的 sqlserver 适配器,以及 TinyTDS 所需的连接信息。

development:
  adapter: sqlserver
  database: AdventureWorks2008
  dataserver: localhost\SQLEXPRESS
  username: ruby
  password: rubyist1

Sqlite3

此应用程序的一个要求是不要更改我们正在“爬取”的数据库的架构。此外,还有很多会话信息需要保留——无法全部放入客户端的会话 cookie 中。我们使用 Sqlite3 来存储独立于我们的 SQL Server 数据库的会话信息,并且 此 gem 提供了连接。要了解如何做到这一点,请阅读“将会话信息存储在单独的数据库中”部分。

Slim

这个 gem 消除了 HTML 脚本的尖括号和结束标签。这是一个简单的轻量级语法示例

doctype
html
  head
    title Database Spider
    = stylesheet_link_tag "application", media: "all"
    = javascript_include_tag "application"
    = csrf_meta_tags
  body
    = yield

请注意,如何使用缩进确定生成 HTML 的结束标签的位置。

Sass

这个 gem 也“轻量化”了 CSS 的描述,并且与 Slim 结合得非常好。您将看到 Slim/Sass 的使用方式是 Slim 和 Sass 标记的结构完全相同。这里有一个从用户表列表标记中截取的简短示例

Slim 标记

.table_list_region
  p Tables:
  .table_list
    ul
      - @tables.each do |table_name|
        li
          = link_to(table_viewer_path(table_name: table_name)) do
            t #{table_name}

请注意 Slim 如何同时处理标记中的 HTML 和 Ruby 脚本!

Sass 标记

.table_list_region
  width: 240px
  height: 700px
  float: left
  p
    text-align: left
    margin-bottom: 2px
  .table_list
    text-align: left
    border: 1px solid
    border-radius: 3px
    width: 100%
    height: 100%
    line-height: 1.5em
    ul
      width: 190px
      height: 99%
      overflow: auto
      margin-top: 2px
      li
        list-style-type: none
        margin-left: -30px
        a:visited
          color: #000000
        a:link
          color: #000000
          text-decoration: none
        a:hover
          color: #0000FF

您可以看到 Sass 标记如何遵循与 Slim 标记相同的结构。这种方法的问题在于它模糊了 CSS 的重用。例如,我可能想重用 table_list 样式,但由于它是 table_list_region 的子级,我无法重用(至少在没有 table_list_region 容器的情况下)。但是,我绝对可以缩进样式

.table_list_region
  width: 240px
  height: 700px
  float: left
  p
    text-align: left
    margin-bottom: 2px

.table_list
  text-align: left
  ... etc ...

而不影响 Slim 标记的结构。现在table_list在容器内是可访问的,也可以被其他容器重用。不幸的是,我至少参与过一个大型开源项目,项目开发者选择不推广样式重用,导致 Sass 结构复杂,我必须精确地遵循 Slim 的层级结构才能找到我想更改的样式。除非您的样式确实是特定于其容器上下文的,否则不要这样做!

Will_paginate

这是一个很棒的 gem,它可以分页您的数据,并提供各种现成的样式以及自定义分页栏样式的能力。当然,使用分页的一个优点是,对于记录数量很大的表,数据库只返回该页的记录,极大地提高了可用性。

代码

我不会详细介绍代码的每一个细节,但会指出一些更有趣的功能。

Schema 类

这个类(schema.rb)封装了我们需要连接到 SQL Server 并获取架构信息所需的静态函数,因此依赖 TinyTDS gem 进行直接 SQL 连接。主要的执行者是这个函数

def self.execute(sql)
  client = create_db_client
  result = client.execute(sql)
  records = result.each(as: :array, symbolize_keys: true)
  array = convert_to_array_of_hashes(result.fields, records)

  array
end

我们依赖这些辅助方法(helpers\my_utils.rb)来创建一个客户端连接

# create a client connection.
def create_db_client
  config = get_current_database_config
  config_as_symbol = symbolize_hash_key(config)
  client = TinyTds::Client.new(config_as_symbol)

  client
end

# Returns the current database config has a dictionary of string => string
def get_current_database_config
  Rails.configuration.database_configuration[Rails.env]
end

# Given a dictionary of string => string, returns :symbol => string
# Example: config_as_symbol = symbolize_hash_key(config)
def symbolize_hash_key(hash)
  hash.each_with_object({}){|(k,v), h| h[k.to_sym] = v}
end

一旦记录从 TinyTDS 返回,我希望将它们打包成一个哈希数组(field => value),以便对结果数据有一个一致的表示。这需要几个后处理函数

# Convert the array of records from the TinyTDS query into an array of hashes, where
# the keys are the field names.
def self.convert_to_array_of_hashes(fields, records)
  array = []
  records.each { |record|
    dict = hash_from_key_value_arrays(fields, record)
    array << dict
  }

  array
end

# Given two arrays of equal length, 'keys' and 'values', returns a hash of key => value
def hash_from_key_value_arrays(keys, values)
  Hash[keys.zip values]
end

Schema 查询

我们现在可以定义 Spider UI 所需的三个函数

  1. get_user_tables
  2. get_parent_table_info_for
  3. get_child_table_info_for

在最后两个函数中,我们将字符串“[table]”替换为表名,然后再执行查询

  sql.sub!('[table]', table_name.partition('.')[2])

目前我们没有处理的是,传入的表名是完全限定的(它也包括了架构名),但查询不考虑架构名,因此我们需要从参数值中提取表名。这是以后要处理的“TODO”项之一。

这些函数依赖于我们将实际查询存储在“queries.yml”文件中,这是一个分层文件,概念上类似于 XML,但在实现上非常不同。在此文件中,我们存储了我们的架构查询

Schema:
  user_tables: "
    select 
      s.name + '.' + o.name as table_name
    from 
      sys.objects o
        left join sys.schemas s on s.schema_id = o.schema_id
    where 
      type_desc = 'USER_TABLE'"

  get_parents: "
    SELECT
      f.parent_object_id as ChildObjectID,
      SCHEMA_NAME(f.schema_id) SchemaName,
      OBJECT_NAME(f.parent_object_id) TableName,
      COL_NAME(fc.parent_object_id,fc.parent_column_id) ColName,
      SCHEMA_NAME(ref.schema_id) ReferencedSchemaName,
      OBJECT_NAME(f.referenced_object_id) ReferencedTableName,
      COL_NAME(fc.referenced_object_id, fc.referenced_column_id) ReferencedColumnName
    FROM 
      sys.foreign_keys AS f
        INNER JOIN sys.foreign_key_columns AS fc ON f.OBJECT_ID = fc.constraint_object_id
        INNER JOIN sys.tables t ON t.object_id = fc.referenced_object_id
        INNER JOIN sys.tables ref ON ref.object_id = f.referenced_object_id
    WHERE
      OBJECT_NAME (f.parent_object_id) = '[table]'"

  get_children: "
    SELECT 
      f.parent_object_id as ChildObjectID,
      SCHEMA_NAME(f.schema_id) SchemaName,
      OBJECT_NAME(f.parent_object_id) TableName,
      COL_NAME(fc.parent_object_id,fc.parent_column_id) ColName,
      SCHEMA_NAME(ref.schema_id) ReferencedSchemaName,
      OBJECT_NAME(f.referenced_object_id) ReferencedTableName,
      COL_NAME(fc.referenced_object_id, fc.referenced_column_id) ReferencedColumnName
    FROM
      sys.foreign_keys AS f
        INNER JOIN sys.foreign_key_columns AS fc ON fc.constraint_object_id = f.OBJECT_ID
        INNER JOIN sys.tables t ON t.OBJECT_ID = fc.referenced_object_id
        INNER JOIN sys.tables ref ON ref.object_id = f.referenced_object_id
    WHERE
      OBJECT_NAME (f.referenced_object_id) = '[table]'"

我们再次使用一个小型辅助函数来检索文本

# Gets the specified query from the config.yml file
# Example: sql = get_query("Schema", "user_tables")
# TODO: cache keys
def get_query(key1, key2)
  sql = YAML.load_file(File.expand_path('config/queries.yml'))
  sql[key1][key2]
end

这里是另一个“TODO”项:最终这个结构将“重构”以包含数据库上下文,因为查询数据库架构是非常数据库特定的!

获取用户表

这个函数简单地返回用户表名的集合

# Returns an array of strings containing the user tables in the database to which we're connecting.
def self.get_user_tables
  client = create_db_client
  sql = get_query("Schema", "user_tables")
  result = client.execute(sql)
  records = result.each(as: :array, symbolize_keys: true)
  names = get_column(records, 0)

  names
end

获取父表信息

这个函数返回当前表的外键关联,结果是一个包含所有父表以及描述父子表外键关系的架构信息的数组。

# Returns an array of hashes (column name => value) of the parent table schemas of the specified table.
def self.get_parent_table_info_for(table_name)
  sql = get_query("Schema", "get_parents")
  sql.sub!('[table]', table_name.partition('.')[2])
  execute(sql)
end

获取子表信息

这个函数返回子表及其外键列关系到指定的父表。这个查询的结构与前一个函数返回的结构相似,这要归功于 SQL 的格式(见前文)。

# Returns an array of hashes (column name => value) of the child table schemas of the specified table.
def self.get_child_table_info_for(table_name)
  sql = get_query("Schema", "get_children")
  sql.sub!('[table]', table_name.partition('.')[2])
  execute(sql)
end

将会话信息存储在单独的数据库中

我们需要在 initializers\session_store.rb 中告诉 Rails 我们想使用数据库而不是 cookie 来存储会话信息

DatabaseSpider::Application.config.session_store :active_record_store

但是,我们不想让 Rails 使用 SQL Server 数据库,所以我们还需要

ActiveRecord::SessionStore::Session.establish_connection(:sessions)

最后,我们需要在 config\database.yml 文件中定义会话

# use sqlite3 as the DB for storing session information.
sessions:
  adapter: sqlite3
  database: db/session.sqlite3
  pool: 5
  timeout: 5000

动态 ActiveRecord

另一个要求是避免为数据库中的每个物理表创建一个具体的 Model 类。为了实现这一点,我们从 ActiveRecord::Base 派生一个类,但指定它关联的表名

class DynamicTable < ActiveRecord::Base

  # Returns an array of records for the specified table.
  def set_table_data(table_name)
    DynamicTable::table_name = table_name
  end

  # Returns the field names given at least one record.
  def get_record_fields(records)
    fields = []
    fields = records[0].attributes.keys if records.count > 0

    fields
  end
end

这使我们可以像使用任何其他 ActiveRecord 实例一样与表进行交互。

get_record_fields 函数用于返回记录的字段名——我们任意选择第一条记录作为字段列表,假设至少存在一条记录。这有点麻烦,因为即使没有返回记录,我们也希望表至少显示字段名。所以“TODO”项之一是使用带有“where 1=0”的 TinyTDS 查询,尽管它返回零行,但会为我们填充字段名。

TableViewerController,Index 函数

TableViewerControllerindex 函数是网站的主要工作负载

def index
  initialize_attributes
  update_model_page_numbers

  if self.table_name
    restore_page_number_on_nav # restores the page number when navigating back along the breadcrumbs
    self.last_page_num = self.model_page_nums[self.table_name+'_page'] # preserve the page number so selected navigation records are selected from the correct page.
    @data_table = load_DDO(self.table_name, self.last_page_num, self.qualifier, MAIN_TABLE_ROWS)
    add_hidden_index_values(@data_table)
    load_navigators(self.table_name)
    @parent_dataset = load_fk_tables(@parent_tables)
    @child_dataset = load_fk_tables(@child_tables)
    # Update the parent tab index based on the existence and value of the selected_parent_table_index parameter
    update_parent_child_tab_indices
  end
end

初始化各种属性,其中最重要的是

  • 用户表列表。
  • 一个 DynamicTable 实例。
  • 从会话中恢复导航面包屑路径。

会话变量

控制器中定义了几个会话变量

attr_session_accessor :table_name         # the selected user table
attr_session_accessor :qualifier          # the qualifier currently being used to filter the selected user table
attr_session_accessor :breadcrumbs        # the breadcrumb trail
attr_session_accessor :last_page_num      # the last page number of the user table
attr_session_accessor :force_page_num     # if set, forces the pagination to a different page, used with breadcrumbs
attr_session_accessor :model_page_nums 	  # dictionary of page numbers for all the models being displayed.

它们有助于在帖子之间保持页面的状态。

自定义属性访问器

我厌倦了不得不写这样的代码

@qualifier = session[:qualifier]
session[:table_name] = @table_name

所以我写了一个自定义属性访问器,它简化了会话值的获取和设置

# Adds an "attr_session_accessor" declarator that, in addition to setting/getting the value
# to the attribute, it also gets/sets the value from the session.
# Usage inside the class defining the attribute: self.foobar = 1
# Note that "self." must prefix the usage of the attribute.
class Class
  def attr_session_accessor(*args)
    args.each do |arg|
      self.class_eval("def #{arg}; @#{arg}=session['#{arg}']; end")
      self.class_eval("def #{arg}=(val); @#{arg}=val; session['#{arg}']=val; end")
    end
  end
end

对于 getter,此代码从会话名称初始化指定属性,相当于(对于变量 qualifier)编写

@qualifier = session[:qualifier]

setter 初始化属性和会话键与指定值,相当于(对于变量 table_name)编写

@table_name = val
session[:table_name] = @table_name

因此,我们可以这样编码(这是单击面包屑的处理程序)

# Navigate back to the selected table in the nav history and pop the stack to that point.
# Use the qualifier that was specified when navigating to this table.
# Restore the page number the user was previously on for this table.
def nav_back
  stack_idx = params[:index].to_i
  breadcrumb = self.breadcrumbs[stack_idx]          # get the current breadcrumb
  self.table_name = breadcrumb.table_name           # we want to go back to this table and its qualifier
  self.qualifier = breadcrumb.qualifier
  self.breadcrumbs = self.breadcrumbs[0..stack_idx] # remove all the other items on the stack
  self.force_page_num = breadcrumb.page_num

  redirect_to table_viewer_path+"/index"
end

我想,如果我给这些变量命名时加上一些前缀,比如“sess_”,让读者清楚我们在访问会话数据,这样会更具可读性,但是,一个更有用的“TODO”是创建一个 Session 类,其中包含控制器引用的所有会话属性。内部,这个类仍然可以使用我的自定义属性访问器,但它也可以让 IDE 中的智能感知工作,从而改善程序员的体验并提供清晰的幕后信息。

字节编码

我遇到的一个问题是 SQL Server 字段使用了字节编码。这给 Rails 带来了麻烦,需要一个助手方法,应用于每个字段值

# Fixes encoding to UTF-8 for certain field types that cause problems.
# http://stackoverflow.com/questions/13003287/encodingundefinedconversionerror
helper_method :fix_encoding
def fix_encoding(value)
  value.to_s.encode('UTF-8', {:invalid => :replace, :undef => :replace, :replace => '?'})
end

隐藏字段

Adventure Works 数据库中的某些字段可以一直隐藏。此外,分页器添加了一个列,它在内部用于跟踪用户正在查看的页面,最后,我添加了一个列,每行的复选框用于识别选定的行。这些都不需要显示,因此我们有几个辅助方法

# Return true if the field can be displayed.
# All sorts of interesting things can be done here:
# Hide primary keys, ModifiedDate, rowguid, etc.
helper_method :display_field?
def display_field?(table_name, field_name)
  # '__rn' is something that will_paginate adds.
  # '__idx' is my hidden column for creating a single column unique ID to identify selected rows, since
  # we can't guarantee single-field PK's and we need some way to identify a row uniquely other than the actual data.
  return false if ['__rn', '__idx', 'rowguid', 'ModifiedDate'].include?(field_name)
  true
end

# Returns only the visible fields
helper_method :get_visible_fields
def get_visible_fields(data_table)
  data_table.fields.keep_if {|f| display_field?(data_table.table_name, f)}
end

用户界面

用户界面包含六个区域

  1. 数据库中的用户表列表
  2. 选定表的数据
  3. 选定表的父表
  4. 选定表的子表
  5. 导航选项
  6. 导航列表(面包屑)

我决定将每个部分放入自己的渲染块中,因此生成的 index.html.slim 文件只是

=form_for @table_viewer do |f|
  = render 'breadcrumbs'
  = render 'user_tables'

  - # The selected table data
  - if !@table_name.nil?
    = render 'parent_tables'
    = render 'selected_table'
    = render 'navigation', f: f
    = render 'child_tables'

- # Restore current page selections
javascript:
  select_fk_tab('#parent_tab', '#parent_tab_content', #{@parent_tab_index}, #{@parent_tables.count})
  select_fk_tab('#child_tab', '#child_tab_content', #{@child_tab_index}, #{@child_tables.count})

面包屑

面包屑是一个可点击的表列表,用户已经通过“导航到父级”和/或“导航到子级”选项导航到这些表。当选择一个面包屑时,为该表的数据限定的任何选择都会恢复,表的数据也会恢复。

Slim 标记

- # The navigation breadcrumbs
.navigation_history
  p = "Nav History:"
  br
  - if @breadcrumbs
    - @breadcrumbs.each_with_index do |breadcrumb, index|
      - table_name = breadcrumb.table_name
      = link_to content_tag(:span, "#{table_name.partition('.')[2]}"), navigate_back_path(index: index), {:class => "button"}, :onclick => 'this.blur();'

用户表

这只是用户表的一个字母排序列表。

Slim 标记

- # The list of all tables.
.table_list_region
  p Tables:
  .table_list
    ul
      - @tables.each do |table_name|
        li
          = link_to(table_viewer_path(table_name: table_name)) do
            t #{table_name}

选定表数据

选定的用户表显示在每行上都有一个复选框,以便用户可以选择一行或多行并导航到子表或父表,其结果数据显示在同一个框中,但由选定内容限定。如果没有进行选择,则显示所有父表或子表数据。表底部是分页器(请参阅父表和子表数据以获取直观示例)。

Slim 标记

- # The selected user table data.
.table_data_region
  p #{@data_table.table_name}
  = render "table_data", data_table: @data_table
  .digg_pagination
    = will_paginate @data_table.records, param_name: @data_table.table_name+"_page"

请注意,此代码(以及用于子表和表导航的代码)都重用了另一个名为“table_data”的渲染。

_table_data.html.slim

这是用于渲染选定表、父表和子表中的表数据的通用文件

- # render for table data. The paginator is separate because it requires additional parameters that are specific
- # to the data being paginated.
.table_data
  - visible_fields = get_visible_fields(data_table)
  table
    - # Display header
    tr
      - # Dummy header column for checkbox
      th

      - # Header of field names
      - visible_fields.each do |field_name|
        th = field_name

    - # Display records
    - data_table.records.each do |record|
      tr class=cycle('row0', 'row1')
        td
          - # __idx is added by the controller to uniquely identify a record.
          = check_box_tag 'selected_records[]', record["__idx"]
          - visible_fields.each_with_index do |field_name, field_index|
            - # Extends the last column out to the right edge of the table.
            - if field_index == visible_fields.length - 1
              td.last = fix_encoding(record[field_name])
            - else
              td = fix_encoding(record[field_name])

此标记的有趣部分是弄清楚何时渲染最后一列,并略微更改样式,以便行颜色延伸到表框的右边缘

td
  border-left: 1px solid #d0d0d0
  white-space: nowrap
  padding-left: 5px
  padding-right: 5px
td.last                         // 最后一列填充所有剩余空间
  width: 100%

还请注意 Ruby 的时髦的 cycle 函数

tr class=cycle('row0', 'row1')

它在行的类样式字符串之间循环,实现交替颜色

tr.row0 // alternating row colors
  background-color: #ffffff
tr.row1
  background-color: #ddffdd

导航

导航区域允许用户选择要导航到的父表或子表。如果用户表显示中选择了行,则父/子记录将由选定的记录限定。在这种情况下,要显示未限定的记录,用户单击“显示所有记录”。要返回限定的记录,请单击最后一个面包屑按钮,因为导航“面包屑”路径会保留限定符。

父导航

选定的用户表被视为子表,具有指向父表的外键。如果用户表中没有选择任何行,则显示父表的所有行。如果选择了一个或多个行(使用复选框),则考虑外键关系到父表的所有列来构建限定性的“where”子句。例如,如果有一个用于账单地址和一个用于送货地址的列引用地址,那么当导航到父表时,父表中的两个地址记录(假设子表的外键 ID 值不同)都将显示。

子导航

这里选定的用户表被视为父表,并且在“导航到子表:”组合框中选定的表被检查其外键字段是否映射到选定的用户表的主键字段。如果用户表中没有选择任何行(使用复选框),则显示所有子记录。如果选择了行,则以编程方式构造限定符以将子记录限制为仅那些引用所选行的记录。

Slim 标记

- # table navigation
.navigation
  fieldset
    legend View and Navigation:
    br
    .nav_button
      = f.submit("Show All Records", name: 'navigate_show_all')
    - # Separate div because 'Go' buttons are left padded.
    .nav_options
      br Navigate to parent:
      = select_tag "cbParents", options_from_collection_for_select(@parent_tables, 'id', 'name')
      = f.submit("Go", name: 'navigate_to_parent')
      br Navigate to child:
      = select_tag "cbChildren", options_from_collection_for_select(@child_tables, 'id', 'name')
      = f.submit("Go", name: 'navigate_to_child')

父表和子表

页面的这两个区域几乎相同,除了分页的处理方式以及它们显示的表和记录。这些表中的复选框目前还没有任何作用。

这个标记的有趣之处在于标签是表行的样式化列,我们使用滚动条来处理屏幕上显示不下的标签。

Slim 标记(仅用于父表)

- # if we have a selected table and it has parent tables, show the parent tables.
- if !@table_name.nil? && @parent_tables.length > 0
  .tab_region
    .tab_list
      table
        tr
          - @parent_tables.each_with_index do |table_info, index|
            td id="parent_tab#{index}"
              a title="View #{table_info.name}" 
		onclick="select_fk_tab('#parent_tab', '#parent_tab_content', #{index}, #{@parent_tables.count})"
                span = "#{table_info.name}"


    - # table_info isn't used because this data is formatted for the comboboxes.
    - # What we're actually simply interested in here is the index.
    - @parent_tables.each_with_index do |table_info, index|
      .tab_content id="parent_tab_content#{index}"
        .tab_table_data_region
          = render "table_data", data_table: @parent_dataset[index]
          .digg_pagination
            = will_paginate @parent_dataset[index].records, 
              param_name: @parent_dataset[index].table_name+"_page", 
              params: {selected_parent_table_index: index }

Javascript

最后,我们需要少量的 Javascript 来将所有内容整合在一起,它负责选择一个选项卡并在帖子或刷新后渲染页面

/* Deselect all the parent tabs and hide all the content, then select the specified tab and show the desired content */
function select_fk_tab(tab_selector, content_selector, index, num_tabs)
{
  for (var i=0; i<num_tabs; i++)
  {
    $(tab_selector + i.toString()).removeClass('current');
    $(content_selector + i.toString()).hide();
  }

  $(tab_selector + index.toString()).addClass('current');
  $(content_selector + index.toString()).show();
}

结论

完成所有工作后,我们已

  • 成功将 Ruby on Rails 与 SQL Server 连接
  • 处理了使用单独的数据库进行会话状态
  • 成功在动态上下文中使用了 ActiveRecord
  • 为通用、基于 Web 的数据库导航器奠定了良好的基础
  • 在此过程中(这也是我的主要目标之一)学习了 Slim 和 Sass 的很多知识。
© . All rights reserved.