diff --git a/odoo/api.py b/odoo/api.py
index cf56718b03241024760350254b67f347d7875b84..b6a7f1acb8ac7aea17fc56710f5d1545ca91f52b 100644
--- a/odoo/api.py
+++ b/odoo/api.py
@@ -827,6 +827,21 @@ class Cache(object):
             except KeyError:
                 pass
 
+    def get_until_miss(self, records, field):
+        """ Return the cached values of ``field`` for ``records`` until a value is not found. """
+        field_cache = self._data[field]
+        key = records.env.cache_key(field) if field.depends_context else None
+        vals = []
+        for record_id in records._ids:
+            try:
+                if key is not None:
+                    vals.append(field_cache[record_id][key])
+                else:
+                    vals.append(field_cache[record_id])
+            except KeyError:
+                break
+        return vals
+
     def get_records_different_from(self, records, field, value):
         """ Return the subset of ``records`` that has not ``value`` for ``field``. """
         field_cache = self._data[field]
diff --git a/odoo/fields.py b/odoo/fields.py
index 3982f9c00daf6522b64c20b1a7d33b4317ea3699..8569558aa1b7ba3981fb8d4aee6b192d08159363 100644
--- a/odoo/fields.py
+++ b/odoo/fields.py
@@ -757,6 +757,15 @@ class Field(MetaField('DummyField', (object,), {})):
         """
         return False if value is None else value
 
+    def convert_to_record_multi(self, values, records):
+        """ Convert a list of values from the cache format to the record format.
+        Some field classes may override this method to add optimizations for
+        batch processing.
+        """
+        # spare the method lookup overhead
+        convert = self.convert_to_record
+        return [convert(value, records) for value in values]
+
     def convert_to_read(self, value, record, use_name_get=True):
         """ Convert ``value`` from the record format to the format returned by
         method :meth:`BaseModel.read`.
@@ -1008,6 +1017,38 @@ class Field(MetaField('DummyField', (object,), {})):
 
         return self.convert_to_record(value, record)
 
+    def mapped(self, records):
+        """ Return the values of ``self`` for ``records``, either as a list
+        (scalar fields), or as a recordset (relational fields).
+
+        This method is meant to be used internally and has very little benefit
+        over a simple call to `~odoo.models.BaseModel.mapped()` on a recordset.
+        """
+        if self.name == 'id':
+            # not stored in cache
+            return list(records._ids)
+
+        if self.compute:
+            # Force the computation of the subset of 'records' to compute. This
+            # is necessary because the values in cache are not valid for the
+            # records to compute. Note that this explicitly prevents an infinite
+            # loop upon recompute, which invokes mapped() on the records, which
+            # fetches record values, which calls flush, which calls recompute.
+            to_compute_ids = records.env.all.tocompute.get(self, ())
+            ids = [id_ for id_ in records._ids if id_ in to_compute_ids]
+            for record in records.browse(ids):
+                self.__get__(record, type(records))
+
+        # retrieve values in cache, and fetch missing ones
+        vals = records.env.cache.get_until_miss(records, self)
+        while len(vals) < len(records):
+            # trigger prefetching on remaining records, and continue retrieval
+            remaining = records[len(vals):]
+            self.__get__(first(remaining), type(remaining))
+            vals += records.env.cache.get_until_miss(remaining, self)
+
+        return self.convert_to_record_multi(vals, records)
+
     def __set__(self, records, value):
         """ set the value of field ``self`` on ``records`` """
         protected_ids = []
@@ -2239,10 +2280,8 @@ class _Relational(Field):
         # base case: do the regular access
         if records is None or len(records._ids) <= 1:
             return super().__get__(records, owner)
-        # multirecord case: return the union of the values of 'self' on records
-        get = super().__get__
-        comodel = records.env[self.comodel_name]
-        return comodel.union(*[get(record, owner) for record in records])
+        # multirecord case: use mapped
+        return self.mapped(records)
 
     def _setup_regular_base(self, model):
         super(_Relational, self)._setup_regular_base(model)
@@ -2425,6 +2464,12 @@ class Many2one(_Relational):
         prefetch_ids = IterableGenerator(prefetch_many2one_ids, record, self)
         return record.pool[self.comodel_name]._browse(record.env, ids, prefetch_ids)
 
+    def convert_to_record_multi(self, values, records):
+        # return the ids as a recordset without duplicates
+        prefetch_ids = IterableGenerator(prefetch_many2one_ids, records, self)
+        ids = tuple(unique(id_ for id_ in values if id_ is not None))
+        return records.pool[self.comodel_name]._browse(records.env, ids, prefetch_ids)
+
     def convert_to_read(self, value, record, use_name_get=True):
         if use_name_get and value:
             # evaluate name_get() as superuser, because the visibility of a
@@ -2694,6 +2739,19 @@ class _RelationalMulti(_Relational):
             corecords = corecords.filtered(Comodel._active_name).with_prefetch(prefetch_ids)
         return corecords
 
+    def convert_to_record_multi(self, values, records):
+        # return the list of ids as a recordset without duplicates
+        prefetch_ids = IterableGenerator(prefetch_x2many_ids, records, self)
+        Comodel = records.pool[self.comodel_name]
+        ids = tuple(unique(id_ for ids in values for id_ in ids))
+        corecords = Comodel._browse(records.env, ids, prefetch_ids)
+        if (
+            Comodel._active_name
+            and self.context.get('active_test', records.env.context.get('active_test', True))
+        ):
+            corecords = corecords.filtered(Comodel._active_name).with_prefetch(prefetch_ids)
+        return corecords
+
     def convert_to_read(self, value, record, use_name_get=True):
         return value.ids
 
diff --git a/odoo/models.py b/odoo/models.py
index 37cd83862299c4a0cbb4a10525e9da6c4aa06042..1f5995636be8b8963c91895b2919954c45616b76 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -5242,7 +5242,7 @@ Record ids: %(records)s
         if isinstance(func, str):
             recs = self
             for name in func.split('.'):
-                recs = recs._mapped_func(operator.itemgetter(name))
+                recs = recs._fields[name].mapped(recs)
             return recs
         else:
             return self._mapped_func(func)
@@ -5279,6 +5279,8 @@ Record ids: %(records)s
         if isinstance(func, str):
             name = func
             func = lambda rec: any(rec.mapped(name))
+            # populate cache
+            self.mapped(name)
         return self.browse([rec.id for rec in self if func(rec)])
 
     def filtered_domain(self, domain):