Rails, add_reference with null constraint on existing table
Tags: 10stips, rails
Published: 2022-06-23 11:04:28

###### **This is [#10stips](https://dev.to/t/10stips) (where you learn to solve coding problems and prevent your mental health with _10 seconds tips_).**

Good morning engineers!

How do you add a column with `null:false`, **setting a default value to existing records**, but making sure that **new records are validated against nil** (instead to be assigned a default value)?

This sounds tricky and I'm here to save your life.

Ready?

## Add reference with constraint `null:false`

Today I wanted to add a validated reference column (aka, foreign key) to an existing table with data. Easy, peasy...

```
add_reference :application_users, :company, index: true, foreign_key: true, null: false
```

Setting `null: false` on company_id column creates a constraint where records MUST always have a default value.

This is exactly what I wanted. And if table is empty, there are no problems.

But, what about the default value on existing records?

The migration shown above will fail without providing one.

```
-- add_reference(:application_users, :company, {:null=>false, :foreign_key=>true, :index=>true})
rails aborted!
StandardError: An error has occurred, this and all later migrations canceled:

PG::NotNullViolation: ERROR:  column "company_id" contains null values

Caused by:
ActiveRecord::NotNullViolation: PG::NotNullViolation: ERROR:  column "company_id" contains null values

Caused by:
PG::NotNullViolation: ERROR:  column "company_id" contains null values
```

If null is not allowed by the constraint, and no other default value is offered, the database has no other way to decide.

And here comes the interesting part!

## Adding a default value, the proper way

If you know for sure that you have a company with id: 1, you could add `default: 1` to set all existing records to belong to that company by default.

But, having multiple environments such as development, staging and production, forces you to use the `Company` model and retrieve a value from it.

So, I decided to do something like this...

```
default_company_id = Company.first.try(:id) || Company.create(name: 'Default company').id
add_reference :application_users, :company, index: true, foreign_key: true, null: false, default: default_company_id
```

But I knew it wasn't over, yet!

I don't want new records to be assigned to `default: default_company_id`. Instead, I want the database to validate the transaction and gracefully fail if no company is provided.

So, I added a line:

```
default_company_id = Company.first.try(:id) || Company.create(name: 'Default company').id
add_reference :application_users, :company, index: true, foreign_key: true, null: false, default: default_company_id

# set default back to nil
change_column_default :application_users, :company_id, nil
```

This sets the default value back to nil... and the constraint is still there, so creating an ApplicationUser without a company_id will raise an exception as expected.

Great! We're done!

## An important sidenote

In general, using the model classes in migrations is considered to be an anti-pattern.

I'm using Rails 4 here and with newer versions of Rails you could create an anonymous class which inherits from the ApplicationRecord and then explicitly set the table name to groups, like this:

```
default_company_id = Class.new(ApplicationRecord)
                            .tap { |c| c.table_name = :companies }
                            .find_or_create_by(name: 'Default company')
                            .id
```

Was it helpful?

Share it with your friends/colleagues/brothers and sisters.

Bye,
M.