我的rails项目从sqlserver到postgres迁移之路
来源:互联网 发布:js 产生2 8随机数 编辑:程序博客网 时间:2024/05/29 16:42
近期想把我们的某些rails项目从sqlserver迁移到postgres,由于我们的rails项目里夹杂很多的find_by_sql,而sqlserver与postgreSQL里很多语法/方法不同,需要sql的翻译处理。没有找到特别合适的插件做这件事情,只好自己动手。
一. 环境:
rails版本:3.0.7; ruby版本:1.8.7;
二. 主要步骤和方法:
1.导数据
利用Navicat for PostgreSQL将数据从sqlserver拷贝到postgreSQL
2.导完数据发现表无id,添加id
添加id有各种方法,你都可以尝试。当然你也可以用我在下面写的ruby代码。
3.添加id后发现id不自增长,为id添加sequence。
具体sql操作方法参可以参考http://tian-wang.iteye.com/blog/1051227。
由于我们已有数据,所以设置sequence时要指定start with n,其中n = max(id) + 1 。
特别要注意的是postgreSQL表名、字段名、序列名等最好不要用大写;序列名大写时会报错: relation "sequence_name" does not exist。
还有一点需要ruby程序员注意的是postgreSQL中的单双引号与ruby中单双引号意义完全不同,一不小心容易出错。
由于项目表数目太多,我写了一段ruby代码来完成这件事情(第二步和第三步),没有优化,先将就用着,至于怎么调用,不用多说了吧,哈哈:
module CommonExtensions module OneTimeUsedForMigrate extend ActiveSupport::Concern #注意:postgreSQL的单双引号 和 ruby的单双引号不同,要特别注意 #postgreSQL表名、字段名、序列名等最好不要用大写 #序列名大写时会报错:ActiveRecord::StatementInvalid: PG::Error: ERROR: relation "tables_id_seq" does not exist module ClassMethods def fixed_for_pk tables = fixed_all_table_names Common.fixed_deal_with_primary_keys(tables) end def fixed_deal_with_primary_keys(tables) tables.each do |table| if fixed_has_primary_key?(table) pk = fixed_get_pk_name(table) else pk = fixed_set_primary_key(table) end if pk sequence_name = fixed_add_sequence(table, pk, true, true) fixed_associate_pk_and_sequence(table, pk, sequence_name) end end end def fixed_add_sequence(table,pk,reset_seq=false,force=false) sequence_name = fixed_get_pk_sequence_name(table,pk) if reset_seq #强制删除主键上的sequence? begin delete_sql = "DROP SEQUENCE \"#{sequence_name}\" CASCADE " Article.find_by_sql(delete_sql) rescue end end if force #删除主键为空的列 delete_null_id_sql = "delete from \"public\".\"#{table}\" where \"#{pk}\" is null" Article.find_by_sql(delete_null_id_sql) end #设置主键从多少开始自增长 base_num = fixed_get_sequence_based_num(table,pk) add_sql = "CREATE SEQUENCE \"#{sequence_name}\" START WITH :base_num INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1 " Article.find_by_sql([add_sql, {:base_num => base_num}]) sequence_name end #序列从?开始增长? def fixed_get_sequence_based_num(table,pk) base_num_record = Article.find_by_sql("select max(\"#{pk}\") as max_num from \"#{table}\"").first base_num = base_num_record ? (base_num_record.max_num.to_i + 1) : 1 end def fixed_associate_pk_and_sequence(table,pk,sequence_name) sql = "alter table \"#{table}\" alter column \"#{pk}\" set default nextval('#{sequence_name}') " Article.find_by_sql(sql) true end #生成序列名 def fixed_get_pk_sequence_name(table,pk = false) adaptive_pk = pk || fixed_get_pk_name(table) "#{table}_#{adaptive_pk}_seq".downcase end #获取主键所在的列的名字(并不是主键名) def fixed_get_pk_name(table) return all_primary_keys[table][:col_name] unless all_primary_keys[table].blank? fixed_get_assumed_pk(table) #{:col_name => pk_record.colname, :type => pk_record.typename, :pk_name => pk_record.pk_name} end #如果已有主键,会出错 def fixed_set_primary_key(table) assumed_pk = fixed_get_assumed_pk(table) return false if assumed_pk.nil? sql = "alter table \"#{table}\" add primary key (\"#{assumed_pk}\") " Article.find_by_sql(sql) assumed_pk end #猜测可能会成为主键的名字 def fixed_get_assumed_pk(table) sql = "select table_schema,table_name,column_name,data_type,column_default,is_nullablefrom information_schema.columns where table_name = '#{table}' " columns = Article.find_by_sql(sql).map{|col| [col.column_name, col.data_type]} possible_pk_cols = ["id","#{table}ID","#{table}_id"] possible_model_name = table.singularize possible_pk_cols << "#{possible_model_name}_id" << "#{possible_model_name}ID" possible_pk_cols.uniq! possible_pk_cols.each do |possible_pk_col| return possible_pk_col if fixed_test_column_is_pk?(possible_pk_col,columns) end nil end def fixed_test_column_is_pk?(test_col,columns) columns.select{|col| col[0] == test_col && col[1].downcase == "integer"}.size > 0 end def fixed_has_primary_key?(table) !Common.all_primary_keys[table].blank? end def fixed_all_table_names system_tables = ["sysdiagrams", #sqlserver系统表 "othertables"] special_tables = [] #特殊表,或者不适用/不想加入本次处理的表 sql = "SELECT pg_catalog.pg_relation_filenode(c.oid) as \"Filenode\", relname as \"TableName\"FROM pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace LEFT JOIN pg_catalog.pg_database d ON d.datname = 'postgres'WHERE relkind IN ('r') AND n.nspname NOT IN ('pg_catalog', 'information_schema') AND n.nspname !~ '^pg_toast'ORDER BY relname" Article.find_by_sql(sql).map(&:TableName) - system_tables - special_tables end #{table_name => {col_name,type,pk_name}} def all_primary_keys sql = "select pg_constraint.conname as pk_name,pg_class.relname as table_name,pg_attribute.attname as colname,pg_type.typname as typename frompg_constraint inner join pg_classon pg_constraint.conrelid = pg_class.oidinner join pg_attribute on pg_attribute.attrelid = pg_class.oidand pg_attribute.attnum = pg_constraint.conkey[1]inner join pg_type on pg_type.oid = pg_attribute.atttypidwhere pg_constraint.contype='p'--and pg_class.relname = 'yourtablename' " res = {} pk_records = Article.find_by_sql(sql) pk_records.each do |pk_record| res[pk_record.table_name] = {:col_name => pk_record.colname, :type => pk_record.typename, :pk_name => pk_record.pk_name} end res end end endend
4.开始大头活,做翻译工作
a. 调整top n
top n => limit n
#todo:目前对"top ?"支持不好,sql最好不要写成这种形式,尽量用 top :top 这种形式 def adjust_top_part(sql) focus_reg = /\s+top\s+(\d+|:\w+|\?)/ focus_match_sql = sql.match focus_reg return sql.gsub(focus_reg,"") + " limit #{focus_match_sql[1]} " if focus_match_sql sql end
b. 调整getdate
getdate => CURRENT_DATE
def adjust_get_date(sql) sql.gsub(/getdate\(\s*\)/i," CURRENT_DATE ") end
c. 调整is_null
is_null => COALESCE
def adjust_is_null(sql) sql.gsub(/isnull/i,"COALESCE") end
d. 调整datediff
datediff => 我采取的方法是调整rails中的sql的同时,为postgreSQL添加datediff函数
def adjust_date_diff(sql) focus_reg = /datediff\s*\(.*?(\,)/ sql.gsub(focus_reg){|match| "datediff(" + self.adjust_date_str_for_date_diff(match.gsub(/datediff\s*\(/,"").gsub(/\s*\,$/,"")) + "," } end
-- Function: datediff(character varying, timestamp without time zone, timestamp without time zone)-- DROP FUNCTION datediff(character varying, timestamp without time zone, timestamp without time zone);CREATE OR REPLACE FUNCTION datediff(arg_mode character varying, arg_d2 timestamp without time zone, arg_d1 timestamp without time zone) RETURNS integer AS$BODY$BEGINif arg_mode = 'dd' or arg_mode = 'd' or arg_mode = 'day' or arg_mode = 'dy' or arg_mode = 'w' then return cast(arg_d1 as date) - cast(arg_d2 as date);elsif arg_mode = 'ww' or arg_mode = 'week' then return ceil( ( cast(arg_d1 as date) - cast(arg_d2 as date) ) / 7.0);elsif arg_mode = 'mm' or arg_mode = 'm' or arg_mode = 'month' then return 12 * (date_part('year',arg_d1) - date_part('year',arg_d2)) + date_part('month',arg_d1) - date_part('month',arg_d2) + case when date_part('day',arg_d1) > date_part('day',arg_d2) then 0 when date_part('day',arg_d1) = date_part('day',arg_d2) and cast(arg_d1 as time) >= cast(arg_d2 as time) then 0 else -1 end;elsif arg_mode = 'yy' OR arg_mode = 'y' OR arg_mode = 'yyyy' or arg_mode = 'year' then return (cast(arg_d1 as date) - cast(arg_d2 as date)) / 365;end if;END $BODY$ LANGUAGE plpgsql;
f. 调整column like ‘%condition%’
column like ‘%condition%’ => column::text like '%condition%' (这里用sql标准函数cast(column as type)可能会更好)
#todo: to modify this method to accept more db_column_name def adjust_like_str(sql) db_column_name = "columncode" focus_reg = /#{db_column_name}\s+like/ sql.gsub(focus_reg,"#{db_column_name}::text like" ) end
g. 重写存储过程
rails项目中尽量不要用存储过程,不便于维护,如果是处于性能考虑可以在采取各种优化手段,这个没啥好说的,动手干吧。
h. 重写标量值函数
和上一步一样,体力活。
i. 其他sql函数
根据项目中用到的sql情况添加,比如left():
-- Function: "left"(integer, integer)-- DROP FUNCTION "left"(integer, integer);CREATE OR REPLACE FUNCTION "left"(integer, integer) RETURNS integer AS$BODY$DECLARE num ALIAS FOR $1; str text := ''; n ALIAS FOR $2;BEGINstr := num::text;if n >= length(str) thenreturn str::int;end if;return substring(str from 1 for n)::int;END;$BODY$ LANGUAGE plpgsql;
5.调整find_by_sql、count_by_sql等
将翻译sql的工作隐藏到find_by_sql、count_by_sql等方法中
module ActiveRecord class Base class << self def find_by_sql(sql) return nil if sql.blank? adjusted_sql = adjust_sql_of_find_by_sql(sql) if need_adjusted? connection.select_all(sanitize_sql(adjusted_sql || sql), "#{name} Load").collect! { |record| instantiate(record) } end def count_by_sql(sql) return nil if sql.blank? adjusted_sql = adjust_sql_of_find_by_sql(sql) if need_adjusted? connection.select_value(sanitize_conditions(adjusted_sql || sql), "#{name} Count").to_i end private def need_adjusted? connection.class.to_s.downcase.include? 'postgres' end def adjust_sql_of_find_by_sql(sql) case sql when Array; sql_dup = sql.dup sql_dup[0] = Common.adjust_sql_to_postgre(sql[0]) sql_dup when Hash; sql else Common.adjust_sql_to_postgre(sql) end end end endend
6.其他地方的小调整
目前已经通过本地测试,等待预上线测试,更期待测试报告,哈哈。
pp.prog.pro@hotmail.com (hanpp@fwxgx.com已停用)
- 我的rails项目从sqlserver到postgres迁移之路
- PostgreSQL 数据迁移 (从Mysql到Postgres)
- 项目从oracle迁移到sqlserver后,代码中分页的相对改动 编辑
- 我做的一个web项目从tomcat迁移到weblogic遇到的问题
- 【腾讯TMQ】从Ant到Gradle的迁移之路
- 数据迁移:从SqlServer到Oracle
- 【数据库】从mysql迁移到sqlserver
- oracle pl/sql到postgres plpgsql的迁移
- 从VC6项目到Visual Studio 2005以上的迁移
- 为什么我们要从 NodeJS 迁移到 Ruby on Rails
- 为什么我们要从 NodeJS 迁移到 Ruby on Rails
- 从oracle迁移到SQLserver的陷阱@http://www.chinabyte.com/biz/cbfwq/238/2600238.shtml
- 从oracle到Sqlserver数据库的数据迁移(图文详解)
- [原创][从mambo到joomla的迁移实战之三]网站由mambo迁移到joomla
- 我的RUBY之路--第二周(02) CSS/HTML Rails上传到服务器
- SQLSERVER到ORACLE的数据库迁移
- 从tomcat迁移项目到geronimo
- J2EE项目从MyEclipse迁移到Eclipse
- 一个APP下载升级的Demo(通知栏实时更新下载进度)------(一)
- 如何插入鼠标后禁用笔记本的触板
- 节点创建删除替换
- UCGUI LISTBOX里调用_CallOwnerDraw时出现的问题.
- jquery自定义函数
- 我的rails项目从sqlserver到postgres迁移之路
- ↖(^ω^)↗
- interrupt
- linux 下常使用到的命令
- Struts2上传文件提示上传文件过大(FileUploadBase$SizeLimitExceededException)
- TCP工作原理及参数设置
- 什么是架构——《一线架构师实践指南》笔记
- 选择图片
- oracle 中存储过程一些小应用