Giter Club home page Giter Club logo

arel-helpers's Introduction

Unit Tests

arel-helpers

Useful tools to help construct database queries with ActiveRecord and Arel.

Installation

gem install arel-helpers

Usage

require 'arel-helpers'

ArelTable Helper

Usually you'd reference database columns in Arel via the #arel_table method on your ActiveRecord models. For example:

class Post < ActiveRecord::Base
  ...
end

Post.where(Post.arel_table[:id].eq(1))

Typing ".arel_table" over and over again can get pretty old and make constructing queries unnecessarily verbose. Try using the ArelTable helper to clean things up a bit:

class Post < ActiveRecord::Base
  include ArelHelpers::ArelTable
  ...
end

Post.where(Post[:id].eq(1))

JoinAssociation Helper

Using pure Arel is one of the only ways to do an outer join with ActiveRecord. For example, let's say we have these two models:

class Author < ActiveRecord::Base
  has_many :posts

  # attribute id
  # attribute username
end

class Post < ActiveRecord::Base
  belongs_to :author
  has_many :comments

  # attribute id
  # attribute author_id
  # attribute subject
end

class Comment < ActiveRecord::Base
  belongs_to :post
  belongs_to :author

  # attribute id
  # attribute post_id
  # attribute author_id
end

A join between posts and comments might look like this:

Post.joins(:comments)

ActiveRecord introspects the association between posts and comments and automatically chooses the right columns to use in the join conditions.

Things start to get messy however if you want to do an outer join instead of the default inner join. Your query might look like this:

Post.joins(
  Post.arel_table.join(Comment.arel_table, Arel::Nodes::OuterJoin)
    .on(Post[:id].eq(Comment[:post_id]))
    .join_sources
)

Such verbose. Much code. Very bloat. Wow. We've lost all the awesome association introspection that ActiveRecord would otherwise have given us. Enter ArelHelpers.join_association:

Post.joins(
  ArelHelpers.join_association(Post, :comments, Arel::Nodes::OuterJoin)
)

Easy peasy.

Note that pretty much anything you can pass to ActiveRecord's #join method you can also pass to #join_association's second argument. For example, you can pass a hash to indicate a set of nested associations:

Post.joins(
  ArelHelpers.join_association(Post, { comments: :author })
)

This might execute the following query:

SELECT "posts".*
FROM "posts"
INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"
INNER JOIN "authors" ON "authors"."id" = "comments"."author_id"

#join_association also allows you to customize the join conditions via a block:

Post.joins(
  ArelHelpers.join_association(Post, :comments, Arel::Nodes::OuterJoin) do |assoc_name, join_conditions|
    join_conditions.and(Post[:author_id].eq(4))
  end
)

But wait, there's more! Include the ArelHelpers::JoinAssociation concern into your models to have access to the join_association method directly from the model's class:

include ArelHelpers::JoinAssociation

Post.joins(
  Post.join_association(:comments, Arel::Nodes::OuterJoin) do |assoc_name, join_conditions|
    join_conditions.and(Post[:author_id].eq(4))
  end
)

Query Builders

ArelHelpers also contains a very simple class that's designed to provide a light framework for constructing queries using the builder pattern. For example, let's write a class that encapsulates generating queries for blog posts:

class PostQueryBuilder < ArelHelpers::QueryBuilder
  def initialize(query = nil)
    # whatever you want your initial query to be
    super(query || post.unscoped)
  end

  def with_title_matching(title)
    reflect(
      query.where(post[:title].matches("%#{title}%"))
    )
  end

  def with_comments_by(usernames)
    reflect(
      query
        .joins(comments: :author)
        .where(author[:username].in(usernames))
    )
  end

  def since_yesterday
    reflect(
      query.where(post[:created_at].gteq(Date.yesterday))
    )
  end

  private

  def author
    Author
  end

  def post
    Post
  end
end

The #reflect method creates a new instance of PostQueryBuilder, copies the query into it and returns the new query builder instance. This allows you to chain your method calls:

PostQueryBuilder.new
  .with_comments_by(['camertron', 'catwithtail'])
  .with_title_matching("arel rocks")
  .since_yesterday

Conditional reflections

If you have parts of a query that should only be added under certain conditions you can return reflect(query) from your method. E.g:

  def with_comments_by(usernames)
    if usernames
      reflect(
        query.where(post[:title].matches("%#{title}%"))
      )
    else
      reflect(query)
    end
  end

This can become repetitive, and as an alternative you can choose to prepend not_nil to your method definition:

  class PostQueryBuilder < ArelHelpers::QueryBuilder
    not_nil def with_comments_by(usernames)
      reflect(query.where(post[:title].matches("%#{title}%"))) if usernames
    end
  end

Requirements

Requires ActiveRecord >= 3.1.0, < 8. Depends on SQLite for testing purposes.

Running Tests

bundle exec rspec

Authors

arel-helpers's People

Contributors

camertron avatar ersatzryan avatar hasghari avatar jprosevear avatar lunks avatar michaelherold avatar mikevic avatar net1957 avatar peeja avatar petergoldstein avatar pgeraghty avatar ramhoj avatar rdunlop avatar vkill avatar wycleffsean avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

arel-helpers's Issues

Support for Rails 5.2

Apparently Rails 5.2 changed its internal "join dependency" structure quite a lot and the library doesn't work with it.

I've managed to track the issue from https://github.com/camertron/arel-helpers/blob/master/lib/arel-helpers/join_association.rb#L22 which calls https://github.com/camertron/arel-helpers/blob/master/lib/arel-helpers/join_association.rb#L88 (ActiveRecord::Associations::JoinDependency.new(table, associations, [])).

The signature of that method changed quite a bit in Rails 5.2: https://github.com/rails/rails/blob/v5.2.0/activerecord/lib/active_record/associations/join_dependency.rb#L91

unfortunately it's not well documented.

It seems they've introduced a new argument in this commit rails/rails@ea09bf5 and the concept of alias tracker

breaks subscript notation for has_manys

Hi, i like this gem a lot, but unfortunately it appears to break in this situation:

class Comments < AR::Base
  belongs_to :user
  include ArelHelpers::ArelTable
end
user.comments[0] #=> Now returns an Arel::Attribute, not the first comment

Is there any way to fix this?

Bound parameter error when joining polymorphic relations

Given Rails 4.2.x (not sure if version matters):

class CardLocation < ActiveRecord::Base
  belongs_to :location
  belongs_to :card, polymorphic: true
end
class Location < ActiveRecord::Base
  has_many :card_locations
  has_many :community_tickets, through: :card_locations, source: :card, source_type: "CommunityTicket"
end
class CommunityTicket < ActiveRecord::Base
  has_many :card_locations, as: :card, source_type: 'CommunityTicket'
  has_many :locations, through: :card_locations
end

When executing:

Location.joins(
  ArelHelpers.join_association(Location, :community_tickets)
).to_sql

The output will be (card_type is not bound):

SELECT "locations".* 
FROM "locations" 
INNER JOIN "card_locations" ON "card_locations"."location_id" = "locations"."id" AND "card_locations"."card_type" =  
INNER JOIN "community_tickets" ON "community_tickets"."id" = "card_locations"."card_id"

Expected output (..._type is bound from the association's value of source_type:

SELECT "locations".* 
FROM "locations" 
INNER JOIN "card_locations" ON "card_locations"."location_id" = "locations"."id" AND "card_locations"."card_type" = 'CommunityTicket'
INNER JOIN "community_tickets" ON "community_tickets"."id" = "card_locations"."card_id"

The problem appears to be that the resulting query generates a parameter slot that is not satisfied in the call to join_association when polymorphic associations are in play. This becomes apparent when mixed in to another query that uses parameters, as the "card_type" value is filled in with a parameter from the outside query.

I'm guessing that source_type needs to be pulled and bound from the association's reflection, but I'm not sure where to do this.

Using CONCAT_WS

Hi all, this is an impressive gem.

Can you search multiple columns at the same time? For example, I'd like to search "John Smith" but I currently have to split the search parameter into two words and perform one search against first_name and one search against last_name (example below).

Using the Rails console, I see it's possible to use Postgres to search concatenated fields where "John Smith" will return a result:
User.where("CONCAT_WS(' ', first_name, last_name) LIKE ?", "%#{search_phrase}%").

Does anyone know how to transform the line above so that it can fit as an OR clause below?

arel_query = query.joins(
    ArelHelpers.join_association(@klass, :comments)
  ).joins(
    ArelHelpers.join_association(@klass, :user)
  ).where(
    Post[:title].matches("%#{search_phrase}%")
    .or(Comment[:body].matches("%#{search_phrase}%"))
    # .or(User[:first_name].matches("%#{search_phrase.split(' ')[0]}%"))
    # .or(User[:last_name].matches("%#{search_phrase.split(' ')[-1]}%"))
  )

Rails/Activerecord 7 Support

A gem I'm using has arel-helpers as a dependency. I was on Rails 7.0.0.alpha2 and had no issues with your gem or dependencies. Now that I upgraded to Rails 7.0.0 I get the following error

arel-helpers was resolved to 1.1.0, which depends on
        activerecord (~> 3.0)

    rails (~> 7.0.0) was resolved to 7.0.0, which depends on
      activerecord (= 7.0.0)

It looks like your gem requires activerecord (>= 3.1.0, < 7). I'm not sure why it worked with 7.0.0.alpha2 but now it's throwing any error. Any plan to support activerecord 7?

How to chain methods with OR instead of AND ?

Very interesting gem. I have a question about the Query Builder. By default, it chains the given methods using AND, e.g:

PostQueryBuilder.new
  .with_comments_by(['camertron', 'catwithtail'])
  .with_title_matching("arel rocks")
  .since_yesterday

Queries Post with comments AND with title AND since yesterday.

But how could I write such a builder that would allow me to make OR statements? Ideally, I would like to be able to have both AND and OR statements in a single query. Thanks.

Nested Select statements in Left Outer Joins & SQL CASE Statements

Two questions,

How would you go about writing nested select statements in a left outer join? Ala:

LEFT JOIN (
  SELECT
    COUNT(*) as some_prop,
    some_table.id 
  FROM some_table
  GROUP BY some_table.prop_id
) join_name ON join_name.id = other_table.id

And also is there a better way to encode a SQL CASE statement like the following?

CASE 
  WHEN LOCATE('STRING1', UPPER(prop)) > 0 THEN 'String1' 
  WHEN LOCATE('STRING2', UPPER(prop)) > 0 THEN 'String2'
  WHEN LOCATE('STRING3', UPPER(prop)) > 0 THEN 'String3' 
  ELSE term_name
END AS name

Rails 6 compatibility ?

Hi,

I use a gem which relies on arel-helpers, so I can't update my app to Rails 6+.
Have you planned on upgrading it to support Rails 6/AR 6 ?

If you already know what could cause issues, I'd be happy to look at it and submit a PR.

Matthias

Breaks subscript notation for array like behavior of ActiveRecord::Relation

Possibly a regression in 2.1.0 (#10), or maybe a slightly different bug

I'm seeing this with fairly normal AR class

class Company < ActiveRecord::Base
  include ArelHelpers::ArelTable
Company.all.class                                                                                                                                     
=> Company::ActiveRecord_Relation
[149] pry(main)> Company.all[0]                                                                                                                                        
=> #<struct Arel::Attributes::Attribute
 relation=
  #<Arel::Table:0x007faa3c4d9f20
   @aliases=[],
   @columns=nil,
   @engine=
.....
[150] pry(main)>                   

Arel Model.arel_table.project(stuff) vs Model.select(stuff)

Hi, Thank you for maintaining this awesome gem.
Sorry this is not an issue, just a request for clarification that's hopefully easy to answer.

The Arel project method would appear to be one case where ArelHelpers::ArelTable does not eliminate the need to call .arel_table, and that is OK with me.

But can you pretty much always write Model.select(whatever) instead of Model.arel_table.project(whatever)? Is there a difference? I find using the helpers are blurring the line for me between whether i am aware that i'm writing ActiveRecord, or Arel.

Nested Outer Joins Supported?

Is it possible to write nested outer joins? Eg:

 PurchaseOrder.joins(:purchased_items => {:shipping_items => :shipment})

I would like the joins between purchased_items => shipping_items and shipping_items => :shipment to be outer joins.
I tried a few things without success.

Add LICENSE

Would be nice to have a LICENSE associated with this gem

failure with jbuilder partials

For a long time, I'm using QueryBuilder to construct complicated sql queries.

I'tried to convert my jbuilder view to use partials

It failed because jbuilder call empty? and size methods on partial calls

With this monkey patch:

module ArelHelpers
  class QueryBuilder
    def_delegators :@query, :empty?, :size
  end
end

it's OK

Could you update ?
Regards

SQL error when joining same table using different associations

Given:

class Admin < ActiveRecord::Base
  # attribute name
  # attribute id
end

class Reminder < ActiveRecord::Base
  belongs_to :creator, class_name: :Admin
  belongs_to :receiver, class_name: :Admin
  # attribute creator_id
  # attribute receiver_id
end

When executing:

Reminder.
  joins(ArelHelpers.join_association(Reminder, :creator, Arel::InnerJoin)).
  joins(ArelHelpers.join_association(Reminder, :receiver, Arel::InnerJoin)).
  to_sql

The output will be

ELECT `reminders`.* FROM `reminders` INNER JOIN `admins` ON `admins`.`id` = `reminders`.`creator_id` INNER JOIN `admins` ON `admins`.`id` = `reminders`.`receiver_id

Which will raise error

ActiveRecord::StatementInvalid: Mysql2::Error: Not unique table/alias: 'admins'

Had this on Rails 4.1.x if that matters

NamedFunction with prepared statements

I've just watch your presentation "Advanced aRel: When ActiveRecord Just Isn't Enough" on YouTube. Learned some great stuff there but I have some questions. How can I use NamedFunction with AR prepared statements?

SELECT "tags"."name",
       COUNT(*),
       bool_or("taggings"."tagger_id" = $1)
FROM "tags"
INNER JOIN "taggings" ON "tags"."id" = "taggings"."tag_id"
WHERE "taggings"."taggable_id" = $2
  AND "taggings"."taggable_type" = $3
  AND "taggings"."context" = 'classifications'
GROUP BY name

How would you convert this query to aRel syntax?

Wrong code produced, syntax error, too

My solution for this SQL:

SELECT weights.* 
FROM weights 
WHERE captured_at IN (
SELECT MIN(captured_at) 
FROM weights 
GROUP BY DATE(captured_at))

Was (in my code)โ€ฆ

class Weight < ActiveRecord::Base
  def self.by_created_at_asc
    order created_at: :asc
  end

  def self.by_captured_at_asc
    order captured_at: :asc
  end

  def self.earliest_captured_at
    table
      .project(table[:captured_at].minimum)
      .group(Arel::Nodes::NamedFunction.new('DATE', [table[:captured_at]]))
  end

  def self.first_of_each_day
    where(table[:captured_at].in(earliest_captured_at)).by_captured_at_asc
  end

  def self.table
    self.arel_table
  end
end

Scuttle had emitted this:

Weight.select(Weight.arel_table[Arel.star]).where(
  Weight.arel_table[:captured_at].in(
    Weight.select(Weight.arel_table[:captured_at].minimum).group(
      Arel::Nodes::NamedFunction.new('DATE', [:captured_at])
    ).ast
  )
)

The syntax for NamedFunction is wrong, for one. And the .ast should no longer be passed in. I traced thru Arel to debug this.

Missing 2.14.0 tag on github?

It looks like the git tag for arel-helpers-2.14.0 is missing - that version is available on rubygems though. Could it be added? Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.