def _users_watching_by_filter(self, object_id=None, exclude=None,
                                  **filters):
        """Return an iterable of (``User``/:class:`~tidings.models.EmailUser`,
        [:class:`~tidings.models.Watch` objects]) tuples watching the event.

        Of multiple Users/EmailUsers having the same email address, only one is
        returned. Users are favored over EmailUsers so we are sure to be able
        to, for example, include a link to a user profile in the mail.

        The list of :class:`~tidings.models.Watch` objects includes both
        those tied to the given User (if there is a registered user)
        and to any anonymous Watch having the same email address. This
        allows you to include all relevant unsubscribe URLs in a mail,
        for example. It also lets you make decisions in the
        :meth:`~tidings.events.EventUnion._mails()` method of
        :class:`~tidings.events.EventUnion` based on the kinds of
        watches found.

        "Watching the event" means having a Watch whose ``event_type`` is
        ``self.event_type``, whose ``content_type`` is ``self.content_type`` or
        ``NULL``, whose ``object_id`` is ``object_id`` or ``NULL``, and whose
        WatchFilter rows match as follows: each name/value pair given in
        ``filters`` must be matched by a related WatchFilter, or there must be
        no related WatchFilter having that name. If you find yourself wanting
        the lack of a particularly named WatchFilter to scuttle the match, use
        a different event_type instead.

        :arg exclude: If a saved user is passed in as this argument, that user
            will never be returned, though anonymous watches having the same
            email address may. A sequence of users may also be passed in.

        """
        # I don't think we can use the ORM here, as there's no way to get a
        # second condition (name=whatever) into a left join. However, if we
        # were willing to have 2 subqueries run for every watch row--select
        # {are there any filters with name=x?} and select {is there a filter
        # with name=x and value=y?}--we could do it with extra(). Then we could
        # have EventUnion simply | the QuerySets together, which would avoid
        # having to merge in Python.

        if exclude is None:
            exclude = []
        elif not isinstance(exclude, Sequence):
            exclude = [exclude]

        def filter_conditions():
            """Return joins, WHERE conditions, and params to bind to them in
            order to check a notification against all the given filters."""
            # Not a one-liner. You're welcome. :-)
            self._validate_filters(filters)
            joins, wheres, join_params, where_params = [], [], [], []
            for n, (k, v) in enumerate(iteritems(filters)):
                joins.append(
                    'LEFT JOIN tidings_watchfilter f{n} '
                    'ON f{n}.watch_id=w.id '
                    'AND f{n}.name=%s'.format(n=n))
                join_params.append(k)
                wheres.append('(f{n}.value=%s '
                              'OR f{n}.value IS NULL)'.format(n=n))
                where_params.append(hash_to_unsigned(v))
            return joins, wheres, join_params + where_params

        # Apply watchfilter constraints:
        joins, wheres, params = filter_conditions()

        # Start off with event_type, which is always a constraint. These go in
        # the `wheres` list to guarantee that the AND after the {wheres}
        # substitution in the query is okay.
        wheres.append('w.event_type=%s')
        params.append(self.event_type)

        # Constrain on other 1-to-1 attributes:
        if self.content_type:
            wheres.append('(w.content_type_id IS NULL '
                          'OR w.content_type_id=%s)')
            params.append(ContentType.objects.get_for_model(
                          self.content_type).id)
        if object_id:
            wheres.append('(w.object_id IS NULL OR w.object_id=%s)')
            params.append(object_id)
        if exclude:
            # Don't try excluding unsaved Users:1
            if not all(e.id for e in exclude):
                raise ValueError("Can't exclude an unsaved User.")

            wheres.append('(u.id IS NULL OR u.id NOT IN (%s))' %
                          ', '.join('%s' for e in exclude))
            params.extend(e.id for e in exclude)

        def get_fields(model):
            if hasattr(model._meta, '_fields'):
                # For django versions < 1.6
                return model._meta._fields()
            else:
                # For django versions >= 1.6
                return model._meta.fields

        User = get_user_model()
        model_to_fields = dict((m, [f.get_attname() for f in get_fields(m)])
                               for m in [User, Watch])
        query_fields = [
            'u.{0}'.format(field) for field in model_to_fields[User]]
        query_fields.extend([
            'w.{0}'.format(field) for field in model_to_fields[Watch]])

        query = (
            'SELECT {fields} '
            'FROM tidings_watch w '
            'LEFT JOIN {user_table} u ON u.id=w.user_id {joins} '
            'WHERE {wheres} '
            'AND (length(w.email)>0 OR length(u.email)>0) '
            'AND w.is_active '
            'ORDER BY u.email DESC, w.email DESC').format(
            fields=', '.join(query_fields),
            joins=' '.join(joins),
            wheres=' AND '.join(wheres),
            user_table=User._meta.db_table)
        # IIRC, the DESC ordering was something to do with the placement of
        # NULLs. Track this down and explain it.

        # Put watch in a list just for consistency. Once the pairs go through
        # _unique_by_email, watches will be in a list, and EventUnion uses the
        # same function to union already-list-enclosed pairs from individual
        # events.
        return _unique_by_email((u, [w]) for u, w in
                                multi_raw(query, params, [User, Watch],
                                          model_to_fields))