上一篇我們用 Ruby on Rails + DB2 建立了 Team Room 基礎版:會員註冊、文檔上傳、分類訂閱、郵件通知。本篇將引入 DB2 pureXML 特性,實現:
cd D:/rails/teamroom
ruby script/generate migration add_user_credentials_columns
db/migrate/008_add_user_credentials_columns.rbclass AddUserCredentialsColumns < ActiveRecord::Migration
def self.up
add_column :users, :userid, :string, limit: 8, null: false
add_column :users, :hash_passwd, :string, null: false
add_column :users, :salt, :string, null: false
add_index :users, :userid, unique: true
end
def self.down
remove_column :users, :userid
remove_column :users, :hash_passwd
remove_column :users, :salt
end
end
rake db:migrate
app/models/user.rbrequire 'digest/sha2'
class User < ActiveRecord::Base
attr_accessor :password
validates_uniqueness_of :userid
validates_confirmation_of :password
validates_length_of :password, within: 6..40
before_save :encrypt_password
def self.authenticate(userid, password)
user = find_by_userid(userid)
return nil unless user
expected = Digest::SHA2.hexdigest(user.salt + password)
(user.hash_passwd == expected) ? user : nil
end
private
def encrypt_password
return if password.blank?
self.salt = SecureRandom.hex(16)
self.hash_passwd = Digest::SHA2.hexdigest(salt + password)
end
end
原設計:Subscription belongs_to Subject,導致一個 Subject 只能屬於一個 Subscription,現實中多用戶想訂閱同一技術主題(如「DB2 pureXML」)卻無法實現。
ruby script/generate migration create_subscriptions_subjects_join_table
class CreateSubscriptionsSubjectsJoinTable < ActiveRecord::Migration
def self.up
create_table :subscriptions_subjects, id: false do |t|
t.integer :subscription_id, null: false
t.integer :subject_id, null: false
end
add_index :subscriptions_subjects, [:subscription_id, :subject_id], unique: true
end
def self.down
drop_table :subscriptions_subjects
end
end
# app/models/subscription.rb
class Subscription < ActiveRecord::Base
belongs_to :user
has_and_belongs_to_many :subjects, join_table: 'subscriptions_subjects'
end
# app/models/subject.rb
class Subject < ActiveRecord::Base
has_and_belongs_to_many :subscriptions, join_table: 'subscriptions_subjects'
end
ruby script/generate migration add_metadata_xml_to_documents
class AddMetadataXmlToDocuments < ActiveRecord::Migration
def self.up
execute "ALTER TABLE documents ADD COLUMN metadata XML"
end
def self.down
execute "ALTER TABLE documents DROP COLUMN metadata"
end
end
<metadata>
<author>Zhang San</author>
<tags>
<tag>DB2</tag>
<tag>pureXML</tag>
<tag>Ruby</tag>
</tags>
<version>1.2</version>
<custom_fields>
<field name="confidentiality">internal</field>
<field name="review_status">approved</field>
</custom_fields>
</metadata>
app/models/document.rbclass Document < ActiveRecord::Base
belongs_to :subject
belongs_to :user
# 將 Ruby Hash 序列化為 XML 字符串寫入 DB
def metadata=(hash)
return super(nil) if hash.nil?
builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
xml.metadata {
xml.author hash[:author]
xml.tags { hash[:tags].each { |t| xml.tag t } } if hash[:tags]
xml.version hash[:version]
if hash[:custom_fields]
xml.custom_fields {
hash[:custom_fields].each { |k,v| xml.field(v, name: k) }
}
end
}
end
super(builder.to_xml)
end
# 讀取時解析 XML 為 Hash
def metadata
return nil if super().blank?
doc = Nokogiri::XML(super())
{
author: doc.at_xpath('//author')&.text,
tags: doc.xpath('//tag').map(&:text),
version: doc.at_xpath('//version')&.text,
custom_fields: doc.xpath('//custom_fields/field').each_with_object({}) { |n,h| h[n['name']] = n.text }
}
end
end
# 純 Ruby 方式(全表掃描,效率低)
Document.all.select { |d| d.metadata&.dig(:tags)&.include?('pureXML') && d.metadata&.dig(:author) == 'Zhang San' }
# 推薦:DB2 原生 XMLExists + XMLQuery 推下去
sql = <<-SQL
SELECT d.*, XMLQUERY('$m/metadata/author/text()' PASSING d.metadata AS "m") AS author_name
FROM documents d
WHERE XMLEXISTS('$m/metadata/tags/tag[. = "pureXML"]' PASSING d.metadata AS "m")
AND XMLQUERY('$m/metadata/author/text()' PASSING d.metadata AS "m") = 'Zhang San'
SQL
Document.find_by_sql(sql)
| 函數 | 用途 | 示例 |
|---|---|---|
XMLEXISTS | WHERE 子句中判斷 XPath 是否匹配 | XMLEXISTS('$m//tag[.="DB2"]' PASSING metadata AS "m") |
XMLQUERY | SELECT 列表中提取標量值 | XMLQUERY('$m/metadata/version/text()' PASSING metadata AS "m") |
XMLTABLE | 將 XML 序列展開為關係表 | XMLTABLE('$m/metadata/tags/tag' PASSING metadata AS "m" COLUMNS tag VARCHAR(50) PATH '.') |
UsersController 新增 register/login 動作,使用 User.authenticateDocumentsController#create 接收表單 tags[]、custom_fields 並賦值 @document.metadata = params[:metadata]check_box_tag 'tags[]', 'DB2' 等渲染標籤多選本篇完成了:
下一篇將探討:XML 索引建立、XQuery 複雜報表、版本控制與審計日誌 等進階主題。