`

在Rails下连接SQL Server 2005 分页

阅读更多

For a Rails/SQLServer application I’m working on, I had to deal with pagination with custom queries because of the different joins. The mislav-will_paginate plugin works great for MySQL, but for SQL Server, the paginated query generated by the current SQL Server Adapter (I’m using activerecord-sqlserver-adapter-1.0.0.9250) does not work very well. The current implementation is targetted really for SQL Server 2000 and older versions since these versions do not have support for ROW_NUMBER() method. It is a major pain in the butt to do pagination with these databases. With the newer SQL Sever 2005, the job is a bit easier. Microsoft implemented the ROW_NUMBER() method with a convoluted syntax to have better support for pagination, but it is still a drag because of the weird syntax.

Semergence wrote in his blog about patching the SQLServerAdapter to support pagination. Based on his post, I improved ActiveRecord::ConnectionAdapters::SQLServerAdapter::add_limit_offset! to make the query work in a more general way with free-form queries, e.g. queries ran with the paginate_by_sql() method provided by mislav-will_paginate

Include this script in your environment.rb file, or an external file and “require” the file within environment.rb.

  1. # monkey-patching SQLServerAdapter to support SQL Server 2005-style pagination   
  2. module ActiveRecord   
  3.   module ConnectionAdapters   
  4.     class SQLServerAdapter   
  5.       def add_limit_offset!(sql, options)   
  6.         puts sql   
  7.         options[:offset] ||= 0   
  8.         options_limit = options[:limit] ? "TOP #{options[:limit]}" : ""  
  9.         options[:order] ||= if order_by = sql.match(/ORDER BY(.*$)/i)   
  10.                               order_by[1]   
  11.                             else  
  12.                               sql.match('FROM (.+?)\b')[1] + '.id'  
  13.                             end  
  14.         sql.sub!(/ORDER BY.*$/i, '')   
  15.         sql.sub!(/SELECT/i, "SELECT #{options_limit} * FROM ( SELECT ROW_NUMBER() OVER( ORDER BY #{options[:order] } ) AS row_num, ")   
  16.         sql << ") AS t WHERE row_num > #{options[:offset]}”   
  17.         puts sql   
  18.         sql   
  19.       end  
  20.     end  
  21.   end  
  22. end  
  # monkey-patching SQLServerAdapter to support SQL Server 2005-style pagination
  module ActiveRecord
    module ConnectionAdapters
      class SQLServerAdapter
        def add_limit_offset!(sql, options)
          puts sql
          options[:offset] ||= 0
          options_limit = options[:limit] ? "TOP #{options[:limit]}" : ""
          options[:order] ||= if order_by = sql.match(/ORDER BY(.*$)/i)
                                order_by[1]
                              else
                                sql.match('FROM (.+?)\b')[1] + '.id'
                              end
          sql.sub!(/ORDER BY.*$/i, '')
          sql.sub!(/SELECT/i, "SELECT #{options_limit} * FROM ( SELECT ROW_NUMBER() OVER( ORDER BY #{options[:order] } ) AS row_num, ")
          sql << ") AS t WHERE row_num > #{options[:offset]}”
          puts sql
          sql
        end
      end
    end
  end

The method above monkey-patches the SQLServerAdapter by overwriting the add_limit_offset! method.

Here’s a custom query that I used and the transformed result:

  1. Resource.paginate_by_sql([   
  2.       %!SELECT  resources.*   
  3.             ,skills_count.skill_count   
  4.         FROM resources   
  5.             ,(   
  6.                 SELECT resource_id   
  7.                     , COUNT(*) AS skill_count   
  8.                 FROM resource_skills   
  9.             WHERE meta_skill_id IN (1,2,3,4,5,6,7,8,9,10)   
  10.                 GROUP BY resource_id   
  11.             ) AS skills_count   
  12.         WHERE resources.is_active = ?   
  13.           AND resources.id = skills_count.resource_id   
  14.         ORDER BY skill_count DESC  
  15.       !, true ], :page => page, :per_page => per_page  
Resource.paginate_by_sql([
      %!SELECT  resources.*
        	,skills_count.skill_count
        FROM resources
        	,(
        		SELECT resource_id
        			, COUNT(*) AS skill_count
        		FROM resource_skills
            WHERE meta_skill_id IN (1,2,3,4,5,6,7,8,9,10)
        		GROUP BY resource_id
        	) AS skills_count
        WHERE resources.is_active = ?
          AND resources.id = skills_count.resource_id
        ORDER BY skill_count DESC
      !, true ], :page => page, :per_page => per_page

With :page = 1, :per_page = 2, the resulted SQL is:

  1. SELECT TOP 2 * FROM ( SELECT ROW_NUMBER() OVER( ORDER BY skill_count DESC ) AS row_num, resources.*   
  2.     ,skills_count.skill_count   
  3.  FROM resources   
  4.     ,(   
  5.         SELECT resource_id   
  6.             , COUNT(*) AS skill_count   
  7.         FROM resource_skills   
  8.  WHERE meta_skill_id IN (1,2,3,4,5,6,7,8,9,10)   
  9.         GROUP BY resource_id   
  10.     ) AS skills_count   
  11.  WHERE resources.is_active = 1   
  12.  AND resources.id = skills_count.resource_id   
  13.   
  14.  ) AS t WHERE row_num > 0  
SELECT TOP 2 * FROM ( SELECT ROW_NUMBER() OVER( ORDER BY skill_count DESC ) AS row_num, resources.*
 	,skills_count.skill_count
 FROM resources
 	,(
 		SELECT resource_id
 			, COUNT(*) AS skill_count
 		FROM resource_skills
 WHERE meta_skill_id IN (1,2,3,4,5,6,7,8,9,10)
 		GROUP BY resource_id
 	) AS skills_count
 WHERE resources.is_active = 1
 AND resources.id = skills_count.resource_id

 ) AS t WHERE row_num > 0

The will_pagination’s COUNT query is

  1. SELECT COUNT(*) FROM (   
  2.  SELECT resources.*   
  3.     ,skills_count.skill_count   
  4.  FROM resources   
  5.     ,(   
  6.         SELECT resource_id   
  7.             , COUNT(*) AS skill_count   
  8.         FROM resource_skills   
  9.  WHERE meta_skill_id IN (21,22)   
  10.         GROUP BY resource_id   
  11.     ) AS skills_count   
  12.  WHERE resources.is_active = 1   
  13.  AND resources.id = skills_count.resource_id   
  14.  ) AS count_table  
SELECT COUNT(*) FROM (
 SELECT resources.*
 	,skills_count.skill_count
 FROM resources
 	,(
 		SELECT resource_id
 			, COUNT(*) AS skill_count
 		FROM resource_skills
 WHERE meta_skill_id IN (21,22)
 		GROUP BY resource_id
 	) AS skills_count
 WHERE resources.is_active = 1
 AND resources.id = skills_count.resource_id
 ) AS count_table

The ORDER BY part is automatically removed from the main query (which becomes a sub-select) by the plugin to speed up the query. This in turns sanatizes the sql so that SQL Server doesn’t not complain about nested “ORDER BY” within a sub-select. Neat!

The only catch with the current add_limit_offset! is that it does not support ALIAS-ing, because the aliasing confuses the reqex to parse out the ORDER BY condition in the OVER() part of the query.

For regular find() queries, here’s a sample result

  1. Resource.find(:first)   
  2. # original query:  SELECT * FROM resources   
  3. # transformed:   SELECT TOP 1 * FROM ( SELECT ROW_NUMBER() OVER( ORDER BY resources.id ) AS row_number, * FROM resources ) AS t WHERE row_num > 0  
Resource.find(:first)
# original query:  SELECT * FROM resources
# transformed:   SELECT TOP 1 * FROM ( SELECT ROW_NUMBER() OVER( ORDER BY resources.id ) AS row_number, * FROM resources ) AS t WHERE row_num > 0

Hope this helps and cheers!

分享到:
评论

相关推荐

    Ruby-SQLServerRailsActiveRecord的SQLServer适配器

    本主题将深入探讨如何在Rails应用中使用SQL Server作为数据存储,特别关注`activerecord-sqlserver-adapter`这个gem,它使得Ruby的ActiveRecord能够与Microsoft SQL Server无缝集成。 `activerecord-sqlserver-...

    使用PetaPoco ORM管理SQLServer,SQLIte,MySQL和ProgreSQL数据库

    "Using-the-PetaPoco-ORM-to-manage-SQLServer-SQLIte.pdf"文档很可能是教程或者指南,详细解释了如何在SQL Server和SQLite数据库上使用PetaPoco。文档可能会涵盖上述所有概念,并提供具体的代码示例。 总结起来,...

    rails-playlists-源码.rar

    在本地开发环境中,可以使用`rails server`命令启动应用。 通过对"rails-playlists-源码"的深入学习,开发者不仅可以理解Rails的基础知识,还能掌握如何在实际项目中运用这些知识,构建出功能完备且易于维护的Web...

    DbEntry.Net3.9参考手册.chm

    接口,目前支持 SqlServer、SQLite、MySql、Access、Firebird、Oracle 等数据库。对于 WEB 开发,它既支持 ASP.NET 2.0 的 DataSource 方 式,也支持 Ruby On Rails 风格的 MVC 方式。  目前,数据库部分已经支持...

    轻量级的 .Net ORM DbEntry.zip

    对于 ORM 和 Sql 调用,它都拥有清晰和易用的接口,目前支持 SqlServer、SQLite、MySql、Access、Firebird、PostgreSQL、Oracle 等数据库。对于 WEB 开发,它既支持 ASP.NET 2.0 的 DataSource 方式,也支持 Ruby On...

    DbEntry.Net4.1教程

    对于 ORM 和 Sql 调用,它都拥有清晰和易用的接口,目前支持 SqlServer、SQLite、MySql、Access、Firebird、Oracle 等数据库。对于 WEB 开发,它既支持 ASP.NET 2.0 的 DataSource 方式,也支持 Ruby On Rails 风格...

    MyBatis-Plus入门.pdf

    在数据库方面,MyBatis-Plus支持常见的数据库系统,比如MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、PostgreSQL和SQL Server等。开发人员可以根据自己的需求选择合适的数据源。 MyBatis-Plus的入门和使用也...

    penfold_2.10-0.1.17.zip

    这个项目旨在提升JSP(JavaServer Pages)中JActiveRecord格式的表达式语言能力,从而为开发者带来更高效、更灵活的编程体验。 首先,让我们理解什么是JActiveRecord。JActiveRecord是Java版的ORM(对象关系映射)...

    symfony教程

    2. 数据库兼容性:与MySQL、PostgreSQL、Oracle和Microsoft SQL Server等多数数据库系统兼容。 3. 遵循惯例优于配置原则:开发者仅需配置与惯例不同的部分,即可开始开发。 4. 跟随Web最佳实践和设计模式:框架遵循...

    symfony权威指南

    - **兼容性广泛**:兼容多种数据库系统(如MySQL、PostgreSQL、Oracle、Microsoft SQL Server)及操作系统(如*nix、Windows)。 - **适用范围**: - **企业级应用**:能够适应现有的IT策略和技术架构,适用于...

Global site tag (gtag.js) - Google Analytics