Migrating Many-to-Many Database Relationships into Drupal: A Complete Guide

Migrating Many-to-Many Database Relationships into Drupal: A Complete Guide

Alex Rollin
Alex Rollin
July 14, 2025
Last updated : February 15, 2026
July 14, 2025

Moving complex relational data structures from traditional databases into Drupal requires careful planning and the right approach. Many-to-many relationships, where multiple records in one table connect to multiple records in another, present particular challenges that trip up even experienced developers.

This guide walks through the complete process of migrating these relationships using Drupal's Migrate API. You'll learn how to handle junction tables, create proper entity references, and avoid common pitfalls that can derail your migration project.

Based on project work with clients transitioning from legacy systems, we've found that understanding the fundamental differences between relational database structures and Drupal's entity system is crucial for success. The techniques covered here work with Drupal 11 and apply to migrations from MySQL, PostgreSQL, and other relational databases.

Understanding Many-to-Many Database Relationships in Drupal Migration Context

Traditional relational databases use junction tables to handle many-to-many relationships. For example, an e-commerce system might have:

  • products table: `id`, `name`, `description`
  • categories table: `id`, `name`, `slug`
  • product_categories table: `product_id`, `category_id`

This structure allows products to belong to multiple categories and categories to contain multiple products. The junction table stores the relationship pairs.

Drupal approaches this differently. Instead of junction tables, Drupal uses entity reference fields that can hold multiple values. A product content type would have a multi-value "Categories" field that references taxonomy terms directly.

This architectural difference means your migration strategy must transform junction table data into multi-value field data. The Migrate API provides tools to handle this transformation, but you need to structure your approach correctly.

Preparing Your Source Data for Drupal Migration

The key to successful many-to-many migration lies in data preparation. Rather than migrating the junction table separately, you'll want to aggregate the related data directly in your source query.

Here's how to prepare article-to-tag relationships using SQL aggregation:

SELECT 
  a.id AS article_id,
  a.title,
  a.body,
  a.created,
  GROUP_CONCAT(at.tag_id ORDER BY at.tag_id) AS tag_ids
FROM articles a
LEFT JOIN article_tag at ON a.id = at.article_id  
GROUP BY a.id, a.title, a.body, a.created;

This query produces one row per article with all related tag IDs as a comma-separated string. The `ORDER BY` clause ensures consistent ordering, which helps with debugging and testing.

When working with clients, we recommend testing this aggregation query thoroughly before building your migration. Check for edge cases like articles with no tags, articles with duplicate tag assignments, or unusually long tag lists that might hit database limits.

Setting Up Your Drupal Migration Structure

Successful many-to-many migrations require a specific order of operations. You must migrate the referenced entities (tags) before the referencing entities (articles). This ensures the entity reference fields can properly link to existing entities.

Migrating Referenced Entities First

Start with the simpler migration for your referenced entities:

id: migrate_tags
label: Import Tags
migration_group: content_migration
source:
  plugin: table
  table_name: tags
  fields:
    - id
    - name
    - slug
  ids:
    id:
      type: integer
process:
  name: name
  field_slug: slug
destination:
  plugin: entity:taxonomy_term
  default_bundle: tags

This creates taxonomy terms for each tag in your source database. The migration ID (`migrate_tags`) becomes important for the next step, the article migration will reference this migration by name.

Migrating Articles with Entity References

The article migration handles the many-to-many relationship through the `field_tags` process:

id: migrate_articles
label: Import Articles with Tag References
migration_group: content_migration
migration_dependencies:
  required:
    - migrate_tags
source:
  plugin: table
  table_name: articles
  fields:
    - id
    - title
    - body
    - created
    - tag_ids
  ids:
    id:
      type: integer
process:
  title: title
  body/value: body
  body/format:
    plugin: default_value
    default_value: basic_html
  created: created
  field_tags:
    plugin: explode
    source: tag_ids
    delimiter: ','
    process:
      plugin: migration_lookup
      migration: migrate_tags
      source: '@value'
destination:
  plugin: entity:node
  default_bundle: article

The `field_tags` process does two things:

  • Explode splits the comma-separated tag IDs into an array
  • Migration lookup converts each source tag ID to the corresponding Drupal entity ID

The `migration_dependencies` ensures tags are migrated before articles, preventing reference errors.

Handling Complex Paragraph Relationships in Drupal

Many-to-many relationships become more complex when involving paragraph entities. Consider a recipe system where each recipe has multiple ingredients, and each ingredient contains multiple fields (name, quantity, unit).

Our approach involves creating a custom process plugin to structure the aggregated data properly:

getSourceProperty($field_name));
      foreach ($field_values as $key => $field_value) {
        if (!empty($field_value)) {
          $aggregated_fields[$key][$field_name] = $field_value;
        }
      }
    }
    
    return array_values($aggregated_fields);
  }
}

This plugin takes multiple pipe-separated field values and structures them into arrays suitable for paragraph creation. The corresponding migration configuration uses this plugin with `entity_generate`:

process:
  _recipe_ingredients:
    plugin: recipe_related_fields_aggregator
    source: RecipeIngredientsFields
  field_ingredients:
    - plugin: sub_process
      source: '@_recipe_ingredients'
      process:
        _ingredient_name: ingredient_name
        _quantity: quantity
        _unit: unit
        target_id:
          plugin: entity_generate
          entity_type: paragraph
          bundle: recipe_ingredient
          values:
            field_ingredient_name/0/value: '@_ingredient_name'
            field_quantity/0/value: '@_quantity'
            field_unit/0/value: '@_unit'
        target_revision_id:
          plugin: entity_value
          source: '@target_id'
          entity_type: paragraph
          field_name: revision_id

The `entity_generate` plugin creates paragraph entities on-the-fly during migration, which is perfect for one-off relationships that don't need to be shared across multiple parent entities.

Executing and Managing Your Drupal Migration

Migration execution requires careful attention to order and dependencies. We recommend this workflow for most projects:

Initial Migration Run

# Check migration status
drush migrate:status --group=content_migration

# Import referenced entities first
drush migrate:import migrate_tags

# Import main entities with relationships
drush migrate:import migrate_articles

# Verify results
drush migrate:status --group=content_migration

Handling Updates and Changes

When source data changes, you can update existing migrations rather than starting over:

# Update only changed records
drush migrate:import migrate_tags --update

# Sync changes to articles
drush migrate:import migrate_articles --update

The `--update` flag compares source data with previously migrated content and only processes changes. This saves significant time on large datasets.

Troubleshooting Common Drupal Migration Issues

Missing Entity References

If your entity reference fields appear empty after migration, check these common causes:

Source data issues: Verify your aggregation query produces the expected results. Test with a small subset first.

Migration dependencies: Ensure referenced entities are migrated before referencing entities. Use `migration_dependencies` in your YAML.

Field configuration: Confirm your entity reference field allows multiple values and references the correct entity type.

Performance Problems

Large many-to-many datasets can cause memory issues. Our team uses these strategies:

Batch processing: Configure your migration to process records in smaller batches:

source:
  plugin: table
  table_name: articles
  batch_size: 100

Database optimization: Add indexes to your source junction tables, especially on foreign key columns.

Memory monitoring: Use `drush migrate:import --feedback=100` to monitor progress and catch issues early.

Duplicate References

Junction tables sometimes contain duplicate relationships. Handle this in your aggregation query:

SELECT 
  a.id AS article_id,
  a.title,
  GROUP_CONCAT(DISTINCT at.tag_id ORDER BY at.tag_id) AS tag_ids
FROM articles a
LEFT JOIN article_tag at ON a.id = at.article_id
GROUP BY a.id, a.title;

The `DISTINCT` keyword eliminates duplicates before aggregation.

Advanced Migration Patterns for Complex Relationships

Conditional Relationships

Sometimes you need to migrate relationships based on conditions. Use the `skip_on_empty` process plugin:

field_tags:
  plugin: explode
  source: tag_ids
  delimiter: ','
  process:
    - plugin: skip_on_empty
      method: process
    - plugin: migration_lookup
      migration: migrate_tags
      source: '@value'

This skips empty tag references while processing valid ones.

Cross-Migration Dependencies

Complex systems often require relationships between different migration groups. Use `migration_dependencies` with external migrations:

migration_dependencies:
  required:
    - migrate_users
    - migrate_tags
  optional:
    - migrate_media

Required dependencies must complete successfully before your migration runs. Optional dependencies run if available but don't block your migration.

Validating Your Migration Results

After migration, validate that your many-to-many relationships transferred correctly:

Database Verification

Check that entity reference fields contain the expected data:

SELECT 
  n.nid,
  n.title,
  GROUP_CONCAT(t.name) AS tags
FROM node n
JOIN node__field_tags ft ON n.nid = ft.entity_id
JOIN taxonomy_term_field_data t ON ft.field_tags_target_id = t.tid
WHERE n.type = 'article'
GROUP BY n.nid, n.title
LIMIT 10;

Content Verification

Review a sample of migrated content through the Drupal interface to ensure relationships display correctly and editorial workflows function as expected.

Our team typically validates 5-10% of migrated content manually, focusing on edge cases like entities with many relationships or complex paragraph structures.

Planning Your Next Migration

Successfully migrating many-to-many relationships requires understanding both your source data structure and Drupal's entity system. The techniques covered here work for most relational database migrations, but complex legacy systems may require additional custom plugins or data transformation steps.

When planning your migration project, allocate time for data analysis, testing with small datasets, and thorough validation. Many-to-many relationships often reveal unexpected data quality issues that need attention before full migration.

If you're facing a complex migration with tight deadlines or unusual data structures, consider working with experienced developers who can help navigate the technical challenges while ensuring your content transitions smoothly to Drupal.

Share this article