NoSQL, ORMs & Choosing Storage

Document databases, object-relational mappers, and how to choose the right storage for your application

NoSQL / Document Databases

"NoSQL" is a catch-all term for databases that don't use the traditional table-and-row relational model. The most common type for web development is the document database, where data is stored as flexible JSON-like documents instead of rigid table rows.

MongoDB as Example

MongoDB is the most popular document database. Instead of tables with fixed columns, you have collections of documents that can each have different structures:

// A MongoDB document (stored as BSON — binary JSON) { "_id": ObjectId("507f1f77bcf86cd799439011"), "title": "Build login page", "description": "Add username/password auth", "priority": "high", "status": "todo", "tags": ["auth", "frontend"], // arrays are native "assignee": { // nested objects are native "name": "Alice", "email": "alice@example.com" }, "created_at": ISODate("2025-01-15T10:30:00Z") }

Notice how documents can contain arrays and nested objects natively — something that requires separate tables and JOINs in a relational database.

SQL vs NoSQL Terminology

SQL Concept NoSQL Equivalent (MongoDB)
Database Database
Table Collection
Row Document
Column Field
SELECT find()
INSERT insertOne() / insertMany()
UPDATE updateOne() / updateMany()
DELETE deleteOne() / deleteMany()
JOIN Embedded document or $lookup (aggregation)

When NoSQL vs SQL

Choose SQL (PostgreSQL) When Choose NoSQL (MongoDB) When
Data has a consistent, well-defined structure Data structure varies between records
You need JOINs across related data Related data is naturally nested (embedded docs)
ACID transactions are critical (banking, orders) Eventual consistency is acceptable
Complex queries with aggregation and filtering Simple lookups by key or basic filters
Data integrity constraints are important Schema flexibility is more important than strictness

MongoDB Code Examples

Node.js with MongoDB (using the official driver):

const { MongoClient } = require('mongodb'); const client = new MongoClient(process.env.MONGO_URL || 'mongodb://localhost:27017'); const db = client.db('cse135'); const stories = db.collection('stories'); // Create const result = await stories.insertOne({ title: 'Build login page', priority: 'high', status: 'todo', created_at: new Date() }); // Read all const allStories = await stories.find({}).toArray(); // Read one const story = await stories.findOne({ _id: new ObjectId(id) }); // Update await stories.updateOne( { _id: new ObjectId(id) }, { $set: { status: 'in-progress' } } ); // Delete await stories.deleteOne({ _id: new ObjectId(id) });

PHP with MongoDB (using the mongodb extension):

cse135->stories; // Create $result = $collection->insertOne([ 'title' => 'Build login page', 'priority' => 'high', 'status' => 'todo', 'created_at' => new MongoDB\BSON\UTCDateTime() ]); // Read all $allStories = $collection->find()->toArray(); // Read one $story = $collection->findOne(['_id' => new MongoDB\BSON\ObjectId($id)]); // Update $collection->updateOne( ['_id' => new MongoDB\BSON\ObjectId($id)], ['$set' => ['status' => 'in-progress']] ); // Delete $collection->deleteOne(['_id' => new MongoDB\BSON\ObjectId($id)]); ?>

ORMs and Query Builders

Writing raw SQL for every database operation works, but it's tedious and error-prone for complex applications. ORMs (Object-Relational Mappers) and query builders provide higher-level abstractions that map database rows to objects in your programming language.

What ORMs do:

  • Map tables to classes and rows to objects
  • Generate SQL from method calls (you write code, the ORM writes SQL)
  • Handle relationships — load related data automatically (eager/lazy loading)
  • Manage schema changes via migrations (version-controlled database changes)
  • Abstract database differences — switch between PostgreSQL and MySQL with minimal code changes

Raw SQL vs ORM Comparison

Operation Raw SQL ORM (Sequelize / Eloquent)
Get all SELECT * FROM stories Story.findAll() / Story::all()
Get by ID SELECT * FROM stories WHERE id = $1 Story.findByPk(id) / Story::find($id)
Create INSERT INTO stories (title) VALUES ($1) Story.create({title}) / Story::create([...])
Update UPDATE stories SET status = $1 WHERE id = $2 story.update({status}) / $story->update([...])
Delete DELETE FROM stories WHERE id = $1 story.destroy() / $story->delete()
Filter SELECT * FROM stories WHERE priority = $1 Story.findAll({where: {priority}}) / Story::where('priority', $p)->get()

Popular ORMs and Query Builders

Language Tool Type Notes
Node.js Sequelize ORM (Active Record) Most established Node.js ORM; supports PostgreSQL, MySQL, SQLite
Node.js Prisma ORM (Data Mapper-ish) Modern, type-safe; uses its own schema language; auto-generates client
Node.js Knex Query Builder Not a full ORM — builds SQL from JS, gives you more control
PHP Eloquent ORM (Active Record) Part of Laravel; very expressive; models = tables
PHP Doctrine ORM (Data Mapper) Used by Symfony; more explicit; entities are plain PHP objects

Active Record vs Data Mapper

The two main ORM patterns differ in how tightly your objects are coupled to the database:

Pattern How It Works Examples Trade-off
Active Record Each object knows how to save itself. story.save() writes to the DB directly. Sequelize, Eloquent, Rails ActiveRecord Simple and fast to develop; harder to test in isolation
Data Mapper Objects are plain data. A separate "mapper" handles persistence. Object doesn't know about the DB. Prisma, Doctrine, Hibernate More code upfront; cleaner separation of concerns; easier to test

Choosing Your Storage

Here's a practical decision framework for picking the right storage for your use case:

Use Case Recommended Storage Why
Learning CRUD / tutorials Flat files (JSON) or in-memory Zero setup; focus on HTTP and REST first
Personal project / prototype SQLite Real SQL with no server to manage; one file to deploy
Production web app PostgreSQL Battle-tested, ACID compliant, rich feature set, excellent ecosystem
Content management / CMS PostgreSQL or MongoDB PostgreSQL if structured; MongoDB if content types vary wildly
User sessions / caching Redis Blazing fast reads/writes; built-in TTL (expiration) for sessions
Analytics / event logging Ingestion: flat files (log files) — append-only, fast, no DB overhead
Analysis: PostgreSQL (or specialized: ClickHouse, TimescaleDB)
Log files are ideal for high-speed write capture; databases with SQL aggregation shine when you need to query and analyze the collected data
Mobile app with offline support SQLite (on device) + PostgreSQL (server) SQLite works offline; sync to server when connected
Microservices with different data shapes Mix — each service picks what fits "Polyglot persistence": use the right DB for each service's needs