dogs
table, with individual recordspsql
command by default will try to connect to a database and username that matches your system’s usernamepsql pets
-U
flag followed by the username we would like to use. To connect to the pets
database as pets_user
psql -U pets_user pets
-W
flag.
psql -U pets_user -W pets
(the order of our flags doesn’t matter, as long as any arguments associated with them are together, such as pets_user
directly following -U
in this example)=>
, the user is a normal user=#
, the user is a superuserCREATE USER {username} {WITH options}
command.WITH PASSWORD 'mypassword'
to provide a password for the user we are creating, CREATEDB
to allow the user to create new databases, or SUPERUSER
to create a user with all elevated permissions.CREATE DATABASE {database name} {options}
inside psql to create a new database.WITH OWNER {owner name}
to set another user as the owner of the database we are making.GRANT
and REVOKE
privileges from a database to users or categories of users.REVOKE CONNECT ON DATABASE {db_name} FROM PUBLIC;
, removing all public connection access.GRANT CONNECT ON DATABASE {db_name} FROM {specific user, PUBLIC, etc.};
\l
or \list
command in psql.\dt
command.SERIAL
: autoincrementing, very useful for IDsVARCHAR(n)
: a string with a character limit of n
TEXT
: doesn’t have character limit, but less performantBOOLEAN
: true/falseSMALLINT
: signed two-byte integer (-32768 to 32767)INTEGER
: signed four-byte integer (standard)BIGINT
: signed eight-byte integer (very large numbers)NUMERIC
: or DECIMAL
, can store exact decimal valuesTIMESTAMP
: date and timeUNIQUE
flag indicates that the data for the column must not be repeated.NOT NULL
.PRIMARY KEY
flag on the column definition itself:FOREIGN KEY (foreign_key_stored_in_this_table) REFERENCE {other table} ({other_tables_key_name})
to connect two tables.CREATE TABLE people (
id SERIAL PRIMARY KEY,
first_name VARCHAR(50),
last_name VARCHAR(50)
);
CREATE TABLE pets (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
age SMALLINT,
person_id INTEGER,
FOREIGN KEY (person_id) REFERENCES people (id)
);
CREATE TABLE
and create table
are interpreted the same way. Using capitalization is a good convention in order to distinguish your keywords.pets
is unique from a table named Pets
. In general, we prefer to use all lowercase for our entities to avoid any of this confusion.SELECT ... FROM ...
statement to select data from a single tableSELECT
clause. If we want all columns, we can also use *
FROM
clause-- Selects all columns from the friends table
SELECT
*
FROM
friends;
-- Selects the first_name column from the friends table (remember whitespace is ignored)
SELECT name
FROM friends;
-- Notice here we are indicating that we want the "name" field from the "friends" table as well as the "name" field from the "puppies" table. We indicate the table name by table.column
-- We are also aliasing these fields with the AS keyword so that our returned results have friend_name and puppy_name as field headers
SELECT
friends.name AS friend_name , puppies.name AS puppy_name
FROM
friends
JOIN
puppies ON friends.puppy_id = puppies.id
WHERE
clause on SELECT
, UPDATE
, and DELETE
statements to narrow the scope of the commandWHERE
clause allows us to select or apply actions to records that match specific criteria instead of to a whole table.WHERE
with a couple of different operators when making our comparison
WHERE {column} = {value}
provides an exact comparisonWHERE {column} IN ({value1}, {value2}, {value3}, etc.)
matches any provided value in the IN
statement. We can make this more complex by having a subquery inside of the parentheses, having our column match any values within the returned results.WHERE {column} BETWEEN {value1} AND {value2}
can check for matches between two values (numeric ranges)WHERE {column} LIKE {pattern}
can check for matches to a string. This is most useful when we use the wildcard %
, such as WHERE breed LIKE '%Shepherd'
, which will match any breed that ends in “Shepherd”NOT
operator can also be used for negation in the checks.JOIN
keyword to join two (or more) tables together into a single virtual table-- Here we are joining the puppies table on to the friends table. We are specifying that the comparison we should make is the foreign key puppy_id on the friends table should line up with the primary key id on the puppies table.
SELECT
*
FROM
friends
JOIN
puppies ON friends.puppy_id = puppies.id
INSERT
statement to insert data into a tableINSERT INTO
keywords.VALUES
keyword and each record we are adding. Here’s an example:-- We are providing the table name, then multiple records to insert
-- The values are listed in the order that they are defined on the table
INSERT INTO table_name
VALUES
(column1_value, colum2_value, column3_value),
(column1_value, colum2_value, column3_value),
(column1_value, colum2_value, column3_value);
-- In this example, we want to use the default value for id since it is autoincremented, so we provide DEFAULT for this field
INSERT INTO friends (id, first_name, last_name)
VALUES
(DEFAULT, 'Amy', 'Pond');
-- Alternatively, we can leave it out completely, since the default value will be used if none is provided
INSERT INTO friends (first_name, last_name)
VALUES
('Rose', 'Tyler'),
('Martha', 'Jones'),
('Donna', 'Noble'),
('River', 'Song');
UPDATE
statement to update data in a tableUPDATE
keyword can be used to find records and change their values in our database.UPDATE {table} SET {column} = {new value} WHERE {match condition};
.UPDATE {table} SET ({column1}, {column2}) = ({value1}, {value2}) WHERE {match condition};
-- Updates the pet with id of 4 to change their name and breed
UPDATE
pets
SET
(name, breed) = ('Floofy', 'Fluffy Dog Breed') WHERE id = 4;
DELETE
statement to remove data from a tableDELETE FROM {table} WHERE {condition};
-- Deletes from the pets table any record that either has a name Floofy, a name Doggo, or an id of 3.
DELETE FROM
pets
WHERE
name IN ('Floofy', 'Doggo') OR id = 3;
<
and the |
operators. They perform the same function for us, just in slightly different orders, taking the content of a .sql file and executing in within the psql environment:
psql -d {database} < {sql filepath}
cat {sql filepath} | psql -d {database}
BEGIN
and ending with either COMMIT
or ROLLBACK
.BEGIN;
UPDATE accounts SET balance = balance - 100.00
WHERE name = 'Alice';
UPDATE branches SET balance = balance - 100.00
WHERE name = (SELECT branch_name FROM accounts WHERE name = 'Alice');
UPDATE accounts SET balance = balance + 100.00
WHERE name = 'Bob';
UPDATE branches SET balance = balance + 100.00
WHERE name = (SELECT branch_name FROM accounts WHERE name = 'Bob');
COMMIT;
node-postgres
library to our application with npm install pg
. From there we will typically use the Pool class associated with this library. That way we can run many SQL queries with one database connection (as opposed to Client, which closes the connection after a query).const { Pool } = require('pg');
// If we need to specify a username, password, or database, we can do so when we create a Pool instance, otherwise the default values for logging in to psql are used:
const pool = new Pool({ username: '<<username>>', password: '<<password>>', database: '<<database>>'})
query
method on the Pool instance will allow us to execute a SQL query on our database. We can pass in a string that represents the query we want to runconst allAirportsSql = `
SELECT id, city_id, faa_id, name
FROM airports;
`;
async function selectAllAirports() {
const results = await pool.query(allAirportsSql);
console.log(results.rows);
pool.end(); // invoking end() will close our connection to the database
}
selectAllAirports();
rows
key that points to an array of objects, each object representing a record with field names as keys.query
function allows us to pass a second argument, an array of parameters to be used in the query string. The location of the parameter substitutions are designated with $1
, $2
, etc., to signify the first, second, etc., arguments.const airportsByNameSql = `
SELECT name, faa_id
FROM airports
WHERE UPPER(name) LIKE UPPER($1)
`;
async function selectAirportsByName(name) {
const results = await pool.query(airportsByNameSql, [ `%${name}%` ]);
console.log(results.rows);
pool.end(); // invoking end() will close our connection to the database
}
// Get the airport name from the command line and store it
// in the variable "name". Pass that value to the
// selectAirportsByName function.
const name = process.argv[2];
// console.log(name);
selectAirportsByName(name);
npm init -y
npm install sequelize@^5.0.0 sequelize-cli@^5.0.0 pg@^8.0.0
npx sequelize-cli init
psql
CREATE USER example_user WITH PASSWORD 'badpassword'
CREATE DATABASE example_app_development WITH OWNER example_user
CREATE DATABASE example_app_test WITH OWNER example_user
CREATE DATABASE example_app_production WITH OWNER example_user
CREATEDB
option when we make them, since sequelize will attempt to make the databases with this user. This other approach would look like:
CREATE USER example_user WITH PASSWORD 'badpassword' CREATEDB
npx sequelize-cli db:create
{
"development": {
"username": "sequelize_recipe_box_app",
"password": "HfKfK79k",
"database": "recipe_box_development",
"host": "127.0.0.1",
"dialect": "postgres",
"seederStorage": "sequelize"
},
"test": {
"username": "sequelize_recipe_box_app",
"password": "HfKfK79k",
"database": "recipe_box_test",
"host": "127.0.0.1",
"dialect": "postgres",
"seederStorage": "sequelize"
},
"production": {
"username": "sequelize_recipe_box_app",
"password": "HfKfK79k",
"database": "recipe_box_production",
"host": "127.0.0.1",
"dialect": "postgres",
"seederStorage": "sequelize"
}
}
model:generate
npx sequelize-cli model:generate --name Cat --attributes "firstName:string,specialSkill:string"
allowNull: false
, adding a uniqueness constraint with unique: true
, adding in character limits to fields such as type: Sequelize.STRING(100)
, or specifying a foreign key with references to another table references: { model: 'Categories' }
.npx sequelize-cli db:migrate
npx sequelize-cli db:migrate:undo
npx sequelize-cli db:migrate:undo:all
model:generate
command also created a model file for us. This file is what allows sequelize to transform the results of its SQL queries into useful JavaScript objects for us.The model is where we can specify a validation that we want to perform before trying to run a SQL query. If the validation fails, we can respond with a message instead of running the query, which can be an expensive operation that we know won’t work.
// Before we make changes, sequelize generates the type that this field represents
specification: DataTypes.TEXT
// We can replace the generated format with an object to specify not only the type, but the validations that we want to implement. The validations can also take in messages the respond with on failure and arguments.
specification: {
type: DataTypes.TEXT,
validate: {
notEmpty: {
msg: 'The specification cannot be empty'
},
len: {
args: [10, 100]
msg: 'The specifcation must be between 10 and 100 characters'
}
}
}
belongsTo
, hasMany
, and belongsToMany
methods to set up model-level associations. Doing so is what creates the helpful functionality like addOwner
that we saw in the pets example, a function that automatically generates the SQL necessary to create a petOwner record and supplies the appropriate petId and ownerId.
belongsTo
association on the “many” side, and a hasMany
association on the “one” side:
Instruction.belongsTo(models.Recipe, { foreignKey: 'recipeId' });
Recipe.hasMany(models.Instruction, { foreignKey: 'recipeId' });
In a many-to-many association, we need to have a belongsToMany
on each side of the association. We generally specify a columnMapping object to show the association more clearly:
// In our Owner model
// To connect this Owner to a Pet through the PetOwner
const columnMapping = {
through: 'PetOwner', // joins table
otherKey: 'petId', // key that connects to other table we have a many association with
foreignKey: 'ownerId' // our foreign key in the joins table
}
Owner.belongsToMany(models.Pet, columnMapping);
// In our Pet model
// To connect this Pet to an Owner through the PetOwner
const columnMapping = {
through: 'PetOwner', // joins table
otherKey: 'ownerId', // key that connects to other table we have a many association with
foreignKey: 'petId' // our foreign key in the joins table
}
Pet.belongsToMany(models.Owner, columnMapping);
npx sequelize-cli seed:generate --name add-cats
up
indicates what to create when we seed our database, down
indicates what to delete if we want to unseed the database.For our up, we use the queryInterface.bulkInsert()
method, which takes in the name of the table to seed and an array of objects representing the records we want to create:
queryInterface.bulkDelete()
method, which takes in the name of the table and an object representing our WHERE clause. Unseeding will delete all records from the specified table that match the WHERE clause.// If we want to specify what to remove:
down: (queryInterface, Sequelize) => {
return queryInterface.bulkDelete('<<TableName>>', {
field1: [value1a, value1b, value1c] //...etc.
});
}
// If we want to remove everything from the table:
down: (queryInterface, Sequelize) => {
return queryInterface.bulkDelete('<<TableName>>', null, {});
}
npx sequelize-cli db:seed:all
will run all of our seeder files.npx sequelize-cli db:seed:undo:all
will undo all of our seeding.:all
we can run specific seed filesbuild
and save
, or the combined create
// Constructs an instance of the JavaScript `Cat` class. **Does not
// save anything to the database yet**. Attributes are passed in as a
// POJO.
const newCat = Cat.build({
firstName: 'Markov',
specialSkill: 'sleeping',
age: 5
});
// This actually creates a new `Cats` record in the database. We must
// wait for this asynchronous operation to succeed.
await newCat.save();
// This builds and saves all in one step. If we don't need to perform any operations on the instance before saving it, this can optimize our code.
const newerCat = await Cat.create({
firstName: 'Whiskers',
specialSkill: 'sleeping',
age: 2
})
save
methoddestroy
destroy
on the model itself. By passing in an object that specifies a where clause, we can destroy all records that match that queryconst cats = await Cat.findAll();
// Log the fetched cats.
// The extra arguments to stringify are a replacer and a space respectively
// Here we're specifying a space of 2 in order to print more legibly
// We don't want a replacer, so we pass null just so that we can pass a 3rd argument
console.log(JSON.stringify(cats, null, 2));
where
key takes an object as a value to indicate what we are filtering byconst { Op } = require("sequelize");
Op.ne: Not equal operator
Op.and: and operator
Op.or: or operator
Op.gt and Op.lt: greater than and less than operators
order
points to an array with the fields that we want to order bylimit
key in order to limit our results to a specified numberinclude
key to our options objectconst pet = Pet.findByPk(1, { include: [ PetType, Owner ] });
console.log(
pet.id,
pet.name,
pet.age,
pet.petTypeId,
pet.PetType.type,
pet.Owners
)
include
point to an object that specifies which model
we have an association with, then chaining an association on with another include
notEmpty
, notNull
, len
, isIn
, etc.specification: {
type: DataTypes.TEXT,
validate: {
notEmpty: {
msg: 'The specification cannot be empty'
},
len: {
args: [10, 100]
msg: 'The specifcation must be between 10 and 100 characters'
}
}
}
.transaction
method in order to create our block. The method takes in a callback with an argument to track our transaction id (typically just a simple tx
variable).transaction
key on their options argument which points to our transaction id. This indicates that this operation is part of the transaction block and should only be executed in the database when the whole block executes without error.async function main() {
try {
// Do all database access within the transaction.
await sequelize.transaction(async (tx) => {
// Fetch Markov and Curie's accounts.
const markovAccount = await BankAccount.findByPk(
1, { transaction: tx },
);
const curieAccount = await BankAccount.findByPk(
2, { transaction: tx }
);
// No one can mess with Markov or Curie's accounts until the
// transaction completes! The account data has been locked!
// Increment Curie's balance by $5,000.
curieAccount.balance += 5000;
await curieAccount.save({ transaction: tx });
// Decrement Markov's balance by $5,000.
markovAccount.balance -= 5000;
await markovAccount.save({ transaction: tx });
});
} catch (err) {
// Report if anything goes wrong.
console.log("Error!");
for (const e of err.errors) {
console.log(
`${e.instance.clientName}: ${e.message}`
);
}
}
await sequelize.close();
}
main();