I have a content type with a field collection. I have set up a node migration and a field collection migration. I have 'track_changes' => 1 on both. When I run the import the first time everything works great. I get my node data as well as the field collection data showing correctly. If I edit the source data on the node migration and run the import again it updates the node but the field collection data is deleted. Any suggestions?

Comments

briand44’s picture

When migrate calls node_save during an update the $node object being passed doesn't contain the field collection data. Therefore, when field_collection_field_update() is triggered it thinks that the field collection data has been removed so it deletes it.

I came up with a workaround by writing a prepare function in my migration that loads the original node object and adds the field collection data from it to the $node object being passed to node_save. This probably isn't an ideal fix but is working for now.

public function prepare($node, stdClass $row) {
    // don't run on initial import only on update
    if (isset($node->nid)) {
      // load the original node object
      $old_node = node_load($node->nid);
      // add original field collection data to our current node object
      $node->[field_collection_field_name] = $old_node->[field_collection_field_name];
    }
  }
briand44’s picture

Project: Migrate » Field collection
Version: 7.x-2.6-rc1 » 7.x-1.x-dev

This seems more like a Field collection issue so moving it there.

mscalone’s picture

I had the same problem and the workaround proposed on #1 worked fine with some minor modifications:

  • $node->[field_collection_field_name] must be $node->{'field_collection_field_name'}
  • you need to call the prepare method of the base class: parent::prepare($node, $row);on the first line otherwise you lose the changes in other fields
public function prepare($node, stdClass $row) {
    parent::prepare($node, $row);
    // don't run on initial import only on update
    if (isset($node->nid)) {
        // load the original node object
        $old_node = node_load($node->nid);
        // add original field collection data to our current node object
        $node->{'field_collection_field_name'} = $old_node->{'field_collection_field_name'};
    }
}
alcroito’s picture

Thanks a lot, this also helped me fix the issue of updating a node, and not losing the field collection values!

vlad.dancer’s picture

@mscalone. Thanks. But there is an error "Call to undefined method MyImportMigration::prepare()", so I think parent::prepare($node, $row); is redundant.

Dru18’s picture

I am having the same issue. The first time it works but if I run the exact same migration again, it wipes out the field collection data. Obviously this is a terrible bug nobody seems to care about. I tried the above source codes but the destination class doesn't call Prepare method if it is updating; it doesn't work, either. As #1 said 2 yrs ago, the node save function is triggered with empty objects. At my wit's end, I feel really frustrated.

UPDATE:
After spending some amount of time, I was able to complete the process. In case there's someone who goes through the same. This is for continuing migration process.

If there is any attached field collections or entity references to a node, you have to map those data. In my case, I attache the original (current) data and let the following (dependent) migration takes care of updating.

// Field Mapping
// NOTE: separator uses whatever delimiter you define

// for Entity Reference mapping
$this->addFieldMapping('field_my_entity_reference', 'my_er_list')->separator(',');

// for Field Collection mapping
$this->addFieldMapping('field_my_field_collection', 'my_fc_list')->separator(',');
// Also map revision id
$this->addFieldMapping('field_my_field_collection:revision_id', 'my_er_list')->separator(',');
// the following mapping is necessary in order to map with "field_collection_item" table
$this->addFieldMapping('field_my_field_collection:source_type')->defaultValue('item_id');

And collect the mapping data before migration saves the data.

public function prepareRow($row) {
  if ($row->nid) {
    // collect entity reference data with your private method
    $my_er_list = $this->getMyEntityReferenceList($row->nid);
    $row->my_er_list = implode(',', $my_er_list);

    // collect field collection data with your private method
    $my_fc_list = $this->getMyFieldCollectionList($row->nid);
    $row->my_fc_list = implode(',', $my_fc_list);
  }
}

I tested it by assigning array and map it in order to avoid the delimiting process; this doesn't seem to work.

Hope this helps someone.

rodrigoaguilera’s picture

Hi

I just ran into this issue.
Can you post the method getMyFieldCollectionList so the solution is a bit more clear.

Thanks

nanak’s picture

I just implemented it that way, works perfectly

public function __construct() {
  ...
  $this->addFieldMapping('field_your_field_collection_field_name', 'field_collection_ids')
    ->separator('[/]');
  $this->addFieldMapping('field_your_field_collection_field_name:revision_id', 'field_collection_revision_ids')
    ->separator('[/]');
  $this->addFieldMapping('field_your_field_collection_field_name:source_type')
    ->defaultValue('item_id');
}

public function prepareRow($row) {
  if (parent::prepareRow($row) === FALSE) {
    return FALSE;
  }

  if (isset($row->migrate_map_sourceid1, $row->migrate_map_destid1)) {
    $field_collection_ids = $this->getFieldCollectionIds('field_your_field_collection_field_name', $row->migrate_map_destid1);
    $row->field_collection_ids = implode('[/]', $field_collection_ids['item_ids']);
    $row->field_collection_revision_ids = implode('[/]', $field_collection_ids['revision_ids']);
  }
  return TRUE;
}

 /**
  * Get the ids of the field collections linked to an entity.
  *
  * @param string $field_name
  *   The field_collection field name
  * @param $id
  *   The id of the entity we are importing
  * @return array
  *   An array containing an ids array and a revision_ids array.
  */
protected function getFieldCollectionIds($field_name, $id) {
  $ids = db_select("field_data_$field_name", 'd')
    ->fields('d', array($field_name . '_value', $field_name . '_revision_id'))
    ->condition('entity_id', $id)
    ->execute()
    ->fetchAllKeyed();

  return array(
    'item_ids' => array_keys($ids),
    'revision_ids'=> array_values($ids),
  );
}
oana.hulpoi’s picture

Thanks, @nanak! Your solution works great! Old field collections remain on the host entity and new ones are added (if it is the case) on a subsequent import (update).

BramDriesen’s picture

Sorry to bump this issue. But the solution Nanak proposed does not work when you also have translations in your entity.

It will keep the field collections, but it will add ALL field collection id's to the current item you're migrating. So let's say you're migrating a field on the english entity and you have a translated version in French. It will link all the French items as well on the English entity. On the plus side it does keep the french values as well in the French entity. If I found a migration solution to this I'll post it in a new comment.

EDIT: So still the best way to do it is to use the prepare function. In here you can write a field query to load the original field data and store it again in the entity. A tutorial about this can be found here: http://timonweb.com/posts/loading-only-one-field-from-an-entity-or-node-...

My use case:

  public function prepare($entity, $row) {
    // Save the original field collection data.
    $query = new EntityFieldQuery();
    $query->entityCondition('entity_type', 'commerce_product')
      ->entityCondition('bundle', $entity->type)
      ->propertyCondition('product_id', $this->productId);
    $result = $query->execute();

    if (isset($result['commerce_product'])) {
      $original_product = $result['commerce_product'];

      $field_fc_product_description_info = $field_info = field_info_field('field_fc_product_description');
      $field_fc_technical_specification_info = $field_info = field_info_field('field_fc_technical_specification');

      field_attach_load('commerce_product', $original_product, FIELD_LOAD_CURRENT, array('field_id' => $field_fc_product_description_info['id']));
      field_attach_load('commerce_product', $original_product, FIELD_LOAD_CURRENT, array('field_id' => $field_fc_technical_specification_info['id']));

      $entity->field_fc_product_description = $original_product[$this->productId]->field_fc_product_description;
      $entity->field_fc_technical_specification = $original_product[$this->productId]->field_fc_technical_specification;
    }
  }
nanak’s picture

To support entity translation, you need to pass an array in your field mapping instead of a string, and add another field mapping 'your_field:language', with an array of languages to import, in the same order than the field above.
But if you need to import a multivalue and multilanguage field, you'll have to use prepare()