---
url: https://docs.forestfuture.dev/d1-record/guide/coming-from-rails.md
description: >-
  What's different from Rails' Active Record: queries start from the request,
  camelCase attributes, OrThrow methods, UUID keys, batches instead of
  transactions.
---

# Coming from Rails

Models, validations, callbacks, associations, scopes, and dirty tracking all follow Active Record, down to the default error messages. Here’s what’s different.

## Querying from the Model

```ts
// Rails: User.where(active: true)
const users = await User.where({ isActive: true }).toArray();
```

A Model queries by itself. Its binding, `static override binding = "DB"` on your app’s base class, plays the part of `connects_to`.

A Worker serves many requests at once from one isolate, so nothing about a request is stored on the class. For a D1 Session or per-request logging, run the request in [`withDatabase`](./connecting#using-the-request-s-database). A record keeps the database it came from.

## Naming things

Most names are Rails’ own, in camelCase:

| Rails                                    | d1-record                                         |
| ---------------------------------------- | ------------------------------------------------- |
| `created_at` (attribute)                 | `createdAt`, mapped to `created_at`               |
| `foreign_key: :user_id`                  | `foreignKey: "userId"`                            |
| `save!`, `create!`, `find_by!`           | `saveOrThrow`, `createOrThrow`, `findByOrThrow`   |
| `new_record?`, `persisted?`              | `isNewRecord()`, `isPersisted()`                  |
| `empty?`, `any?`                         | `isEmpty()`, `hasAny()`                           |
| `changed?`, `saved_change_to_email?`     | `isChanged()`, `hasSavedChangeTo("email")`        |
| `rails generate model Post title:string` | `npx d1-record g model Post title:string`         |
| `db/schema.rb`, `rails db:schema:dump`   | `src/db/schema.ts`, `npx d1-record schema`        |
| `class Post < ApplicationRecord`         | `class Post extends ApplicationRecord("posts")`   |
| `scope :active, -> { … }`                | `static active = this.scope((q) => …)`            |
| `has_many :posts, dependent: :destroy`   | `posts = hasMany(Post, { dependent: "destroy" })` |
| `post.author = user`                     | `post.author.set(user)`                           |
| `build_profile`                          | `user.profile.build()`                            |
| `reload_author`                          | `post.reloadAssociation("author")`                |
| `throw :abort`                           | `throw abort("reason")`                           |
| `to_json`                                | `toJSON()`                                        |

Attributes, conditions, and association keys all name attributes, in camelCase. Column names only appear inside `sql` fragments. Foreign keys follow Rails’ conventions, so `User`’s `has_many` uses `userId`.

## Declaring Models

Migrations define the columns, and a Model declares only behavior. Where Rails reads the columns when the app runs, d1-record reads them into `src/db/schema.ts` when you run `d1-record schema` or a generator, so TypeScript knows every attribute’s type.

A Model names its table, since TypeScript can’t derive a class’s types from its name. Scopes are statics, and validations and callbacks go in a `static {}` block:

```ts
import { ApplicationRecord } from "./application-record";

// Rails:
//   class Product < ApplicationRecord
//     scope :in_stock, -> { where(available: true) }
//     validates :name, presence: true
//   end
export class Product extends ApplicationRecord("products", { available: "boolean" }) {
  static inStock = this.scope((q) => q.where({ available: true }));

  static {
    this.validates("name", { presence: true });
  }
}
```

## Writing atomically

There’s no `transaction do … end`, since D1 can’t keep a transaction open across awaits. Instead, [`batch([...])`](./batches) writes a prepared set of statements and records atomically, with records’ `toSave()` and `toDestroy()`, and bulk writes’ `toUpdateAll()`, `toDeleteAll()`, and so on. Generating keys in code makes whole graphs of new records atomic too. See [D1’s limits](./d1).

## Primary keys

::: warning Keys are UUIDs, not integers
`g model` gives each table a `TEXT` `id` generated in code, which is what makes those batches atomic. `--id integer` gives you integer keys that D1 assigns, without that guarantee.
:::

## Generating code

The generators follow `rails generate model` and `rails generate migration`. A column can also have a literal default, `count:integer!=0`, which Rails’ generators can’t express. See [Generators](./generators).

## Smaller differences

* `insert` and `insertAll` raise on duplicates. Skipping them is opt-in, with `{ onDuplicate: "skip" }`.
* `insert` fills in declared defaults, since that’s where keys are generated.
* `merge` replaces the order rather than appending to it.
* `last()` can’t reverse an `sql` fragment order, and throws `IrreversibleOrder`, rather than trying to parse SQL.
* `find(1, 1)` always returns an array, so its return type follows its arguments.
* `dependent:` strategies run after the owner’s `beforeDestroy` callbacks, wherever they’re declared, so an `abort()` always protects the records.
* Records in a batch run all their before-callbacks before any write, and their after-callbacks after all of them.
* Scopes preloaded with `limit` throw, where Rails silently gives wrong results.

## Not built yet

Default scopes, `has_many :through`, polymorphic associations, single-table inheritance, counter caches, `destroy_async`, and i18n of error messages.
