Module: Parse::Associations::HasMany

Included in:
Object
Defined in:
lib/parse/model/associations/has_many.rb

Overview

Parse has many ways to implement one-to-many and many-to-many associations: `Array`, `Parse Relation` or through a `Query`. How you decide to implement your associations, will affect how `has_many` works in Parse-Stack. Parse natively supports one-to-many and many-to-many relationships using `Array` and `Relations`, as described in Parse Relational Data. Both of these methods require you define a specific column type in your Parse table that will be used to store information about the association.

In addition to `Array` and `Relation`, Parse-Stack also implements the standard `has_many` behavior prevalent in other frameworks through a query where the associated class contains a foreign pointer to the local class, usually the inverse of a `belongs_to`. This requires that the associated class has a defined column that contains a pointer the refers to the defining class.

Query-Approach

In this `Query` implementation, a `has_many` association for a Parse class requires that another Parse class will have a foreign pointer that refers to instances of this class. This is the standard way that `has_many` relationships work in most databases systems. This is usually the case when you have a class that has a `belongs_to` relationship to instances of the local class.

In the example below, many songs belong to a specific artist. We set this association by setting :belongs_to relationship from `Song` to `Artist`. Knowing there is a column in `Song` that points to instances of an `Artist`, we can setup a `has_many` association to `Song` instances in the `Artist` class. Doing so will generate a helper query method on the `Artist` instance objects.

class Song < Parse::Object
  property :released, :date
  # this class will have a pointer column to an Artist
  belongs_to :artist
end

class Artist < Parse::Object
  has_many :songs
end

artist = Artist.first

artist.songs # => [all songs belonging to artist]
# equivalent: Song.all(artist: artist)

# filter also by release date
artist.songs(:released.after => 1.year.ago)
# equivalent: Song.all(artist: artist, :released.after => 1.year.ago)

In order to modify the associated objects (ex. `songs`), you must modify their corresponding `belongs_to` field (in this case `song.artist`), to another record and save it.

Options for `has_many` using the `Query` approach are `:as` and `:field`. The `:as` option behaves similarly to the :belongs_to counterpart. The `:field` option can be used to override the derived column name located in the foreign class. The default value for `:field` is the columnized version of the Parse subclass `parse_class` method.

class Parse::User
  # since the foreign column name is :agent
  has_many :artists, field: :agent
end

class Artist < Parse::Object
  belongs_to :manager, as: :user, field: :agent
end

artist.manager # => Parse::User object

user.artists # => [artists where :agent column is user]

When using this approach, you may also employ the use of scopes to filter the particular data from the `has_many` association.

class Artist
  has_many :songs, ->(timeframe) { where(:created_at.after => timeframe) }
end

artist.songs(6.months.ago)
# => [artist's songs created in the last 6 months]

You may also call property methods in your scopes related to the instance. Note: You also have access to the instance object for the local class through a special “i” method in the scope.

class Concert
  property :city
  belongs_to :artist
end

class Artist
  property :hometown
  has_many :local_concerts, -> { where(:city => hometown) }, as: :concerts
end

# assume
artist.hometown = "San Diego"

# artist's concerts in their hometown of 'San Diego'
artist.local_concerts
# equivalent: Concert.all(artist: artist, city: artist.hometown)

You may also omit the association completely, as rely on the scope to fetch the associated records. This makes the `has_many` work as a macro query setting the :scope_only option to true:

class Author < Parse::Object
  property :name
  has_many :posts, ->{ where :tags.in => name.downcase }, scope_only: true
end

class Post < Parse::Object
  property :tags, :array
end

author.posts # => Posts where author's name is a tag
# equivalent: Post.all( :tags.in => artist.name.downcase )

Array-Approach

In the `Array` implemenatation, you can designate a column to be of `Array` type that contains a list of Parse pointers. Parse-Stack supports this by passing the option `through: :array` to the `has_many` method. If you use this approach, it is recommended that this is used for associations where the quantity is less than 100 in order to maintain query and fetch performance. You would be in charge of maintaining the array with the proper list of Parse pointers that are associated to the object. Parse-Stack does help by wrapping the array in a PointerCollectionProxy which provides dirty tracking.

class Artist < Parse::Object
end

class Band < Parse::Object
  has_many :artists, through: :array
end

artist = Artist.first

# find all bands that contain this artist
bands = Band.all( :artists.in => [artist.pointer] )

band = bands.first
band.artists # => [array of Artist pointers]

# remove artists
band.artists.remove artist

# add artist
band.artists.add artist

# save changes
band.save

ParseRelation-Approach

Other than the use of arrays, Parse supports native one-to-many and many-to-many associations through what is referred to as a Parse Relation. This is implemented by defining a column to be of type `Relation` which refers to a foreign class. Parse-Stack supports this by passing the `through: :relation` option to the `has_many` method. Designating a column as a Parse relation to another class type, will create a one-way intermediate “join-list” between the local class and the foreign class. One important distinction of this compared to other types of data stores (ex. PostgresSQL) is that:

1. The inverse relationship association is not available automatically. Therefore, having a column of `artists` in a `Band` class that relates to members of the band (as `Artist` class), does not automatically make a set of `Band` records available to `Artist` records for which they have been related. If you need to maintain both the inverse relationship between a foreign class to its associations, you will need to manually manage that by adding two Parse relation columns in each class, or by creating a separate class (ex. `ArtistBands`) that is used as a join table.

2. Querying the relation is actually performed against the implicit join table, not the local one.

3. Applying query constraints for a set of records within a relation is performed against the foreign table class, not the class having the relational column.

The Parse documentation provides more details on associations, see Parse Relations Guide. Parse-Stack will handle the work for (2) and (3) automatically.

In the example below, a `Band` can have thousands of `Fans`. We setup a `Relation<Fan>` column in the `Band` class that references the `Fan` class. Parse-Stack provides methods to manage the relationship under the RelationCollectionProxy class.

class Fan < Parse::Object
  # .. lots of properties ...
  property :location, :geopoint
end

class Band < Parse::Object
  has_many :fans, through: :relation 
end

band = Band.first

 # the number of fans in the relation
band.fans.count

# get the first object in relation
fan = bands.fans.first # => Parse::User object

# use `add` or `remove` to modify relations
band.fans.add user
bands.fans.remove user

# updates the relation as well as changes to `band`
band.fans.save

# Find 50 fans who are near San Diego, CA
downtown = Parse::GeoPoint.new(32.82, -117.23)
fans = band.fans.all :location.near => downtown

You can perform atomic additions and removals of objects from `has_many` relations using the methods below. Parse allows this by providing a specific atomic operation request. The operation is performed directly on Parse server and NOT on your instance object.

# atomically add/remove
band.artists.add! objects  # { __op: :AddUnique }
band.artists.remove! objects  # { __op: :AddUnique }

# atomically add unique Artist
band.artists.add_unique! objects  # { __op: :AddUnique }

# atomically add/remove relations
band.fans.add! users # { __op: :Add }
band.fans.remove! users # { __op: :Remove }

# atomically perform a delete operation on this field name
# this should set it as `undefined`.
band.op_destroy!("category") # { __op: :Delete }

You can also perform queries against class entities to find related objects. Assume that users can like a band. The `Band` class can have a `likes` column that is a Parse relation to the User class containing the users who have liked a specific band.

class Band < Parse::Object
  # likes is a Parse relation column of user objects.
  has_many :likes, through: :relation, as: :user
end

You can now find all User records who have liked a specific band. In the example below, the `:likes` key refers to the `likes` column defined in the `Band` collection which contains the set of user records.

band = Band.first # get a band

# find all users who have liked this band, where :likes is a column
# in the Band collection - NOT in the User collection.
users = Parse::User.all :likes.related_to => band

You can also find all bands that a specific user has liked.

user = Parse::User.first

# find all bands where this user
# is in the `likes` column of the Band collection
bands_liked_by_user = Band.all :likes => user

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Class Attribute Details

.relationsHash

A hash mapping of all has_many associations that use the ParseRelation implementation.

Returns:

# File 'lib/parse/model/associations/has_many.rb', line 272

Class Method Details

.has_many(key, scope = nil, opts = {}) ⇒ Array<Parse::Object>, ...

Define a one-to-many or many-to-many association between the local model and a foreign class. Options for `has_many` are the same as the BelongsTo.belongs_to counterpart with support for `:required`, `:as` and `:field`. It has additional options.

Examples:

has_many :fans, as: :users, through: :relation, field: "awesomeFans"
has_many :songs
has_many :likes, as: :users, through: :relation
has_many :artists, field: "managedArtists"

Parameters:

  • key (Symbol)

    The pluralized version of the foreign class. Using the :query method, this implies the name of the foreign column that a pointer to this record. Using the :array or :relation method, this implies the name of the local column that contains either an array of Parse::Pointers in the case of :array, or the Parse Relation, in the case of :relation.

  • scope (Proc) (defaults to: nil)

    Only applicable using :query. A proc that can customize the query by applying additional constraints when fetching the associated records. Works similarly as ActiveModel associations described in section Customizing the Query

Options Hash (opts):

  • :through (Symbol)

    The type of implementation to use: :query (default), :array or :relation. If set to `:array`, it defines the column in Parse as being an array of Parse pointer objects and will be managed locally using a PointerCollectionProxy. If set to `:relation`, it defines a column of type Parse Relation with the foreign class and will be managed locally using a RelationCollectionProxy. If set to `:query`, no storage is required on the local class as the associated records will be fetched using a Parse query.

  • :field (Symbol)

    override the name of the remote column to use when fetching the association. When using through :query, this is the column name of the remote column of the foreign class that will be used for matching. When using :array, this is the name of the remote column of the local class that contains an array of pointers to the foreign class. When using :relation, this is the name of the remote column of the local class that contains the Parse Relation.

  • :as (Symbol)

    override the inferred Parse::Object subclass of the association. By default this is inferred as the singularized camel case version of the key parameter. This option allows you to override the typecast of foreign Parse model of the association, while allowing you to have a different accessor name.

Returns:

See Also:

# File 'lib/parse/model/associations/has_many.rb', line 276

Instance Method Details

#relation_changes?Boolean

Returns true if there are pending relational changes for.

Returns:

  • (Boolean)

    true if there are pending relational changes for



574
575
576
# File 'lib/parse/model/associations/has_many.rb', line 574

def relation_changes?
  changed.any? { |key| relations[key.to_sym] }
end

#relation_updatesHash

A hash of all the relation changes that have been performed on this

instance. This is only used when the association uses Parse Relations.

Returns:



563
564
565
566
567
568
569
570
571
# File 'lib/parse/model/associations/has_many.rb', line 563

def relation_updates
  h = {}
  changed.each do |key|
    next unless relations[key.to_sym].present? && send(key).changed?
    remote_field = self.field_map[key.to_sym] || key
    h[remote_field] = send key # we still need to send a proxy collection
  end
  h
end

#relationsHash

A hash list of all has_many associations that use a Parse Relation.

Returns:

See Also:



556
557
558
# File 'lib/parse/model/associations/has_many.rb', line 556

def relations
  self.class.relations
end