[SQL Server] Exame 70-461 – Tópico 4: Criando e Modificando Constraints – Parte 01

Um importante passo, que faz parte do projeto de um banco de dados, é decidir a melhor maneira de como forçar e garantir a integridade dos dados. A integridade pode ser forçada completa e unicamente na camada de aplicação, ou exclusivamente no nível da camada de dados (leia-se banco de dados), ou, ainda, através de uma abordagem híbrida (esta é a abordagem mais comumente utilizada). Discussões e polêmicas à parte, existe um mínimo de “coisas” que você deveria implementar na camada de banco, visando garantir a integridade. Iremos falar sobre isso no decorrer deste post.

Tipos de Integridade de Dados

Conceitualmente existem três tipos de integridade de dados, conforme ilustra a figura abaixo.

image

· Integridade de domínio (coluna): especifica o conjunto de valores válidos para uma determinada coluna, bem como se a coluna aceita nulo ou não.

· Integridade de entidade (linha): requer que cada linha seja identificada de forma única na tabela, ou seja, cada linha possui uma identificação exclusiva. Isto é normalmente chamado de chave primária (primary key).

· Integridade referencial (entre tabelas): mantém um relacionamento consistente entre a Tabela A e a Tabela B. A chave primária da Tabela A existe como uma coluna na Tabela B, onde é conceitualmente chamada de chave-estrangeira. Desta forma, não será possível inserir um valor na coluna chave-estrangeira da Tabela B que não exista na coluna chave-primária da Tabela A. Por exemplo, não será possível inserir na tabela de Vendas o código de um cliente que não exista na tabela de Clientes.

Dentro do SQL Server temos as seguintes opções para forçar e garantir a integridade de dados:

Tipo de dados Define o tipo de dados que a coluna pode armazenar. Exemplo: DATETIME2
Nulidade Determina se ou não um valor deve estar presente em uma coluna
Constraints Define regras que limitam o conjunto de valores que podem ser armazenados em uma coluna ou, no nível da tabela, como os valores em diferentes colunas estão relacionados
Triggers Define um código que é executado automaticamente quando uma tabela é modificada. Através disso podemos aplicar restrições de valores e, inclusive, regras de negócio, dentre outras coisas.

Nulidade de Coluna (NULL / NOT NULL)

Este mecanismo permite forçar a integridade de domínio. Ele define se uma coluna é obrigatória. Existem muitas bases de dados legadas por aí cuja questão da nulidade fica completamente a cargo da aplicação, enquanto que nas tabelas todas as colunas estão setadas para aceitar nulo. Este tipo de design é um risco!

Vejamos como implementar a nulidade em tabelas utilizando as opções NULL e NOT NULL. Neste ponto estou destruindo e recriando o banco DBExame70461.

use master
GO

if db_id('DBExame70461') IS NOT NULL
BEGIN
    alter database DBExame70461 set single_user with rollback immediate;
    drop DATABASE DBExame70461;
END;

CREATE DATABASE DBExame70461;
GO

USE DBExame70461;
GO

CREATE TABLE dbo.Produto
(    Id INT PRIMARY KEY,
    Numero INT,
    Nome VARCHAR(50) NOT NULL,
    Descricao VARCHAR(500) NULL
);
INSERT dbo.Produto (Id, Nome) VALUES (1, 'Melancia');

Explicitamente defini a coluna NOME como obrigatória e a coluna DESCRICAO como opcional. E a coluna NUMERO, é obrigatória? Por padrão quando não explicitamos NULL ou NOT NULL a coluna será nulável. E a coluna ID? Neste caso, a coluna foi definida como chave-primária e, por consequência, ela será implicitamente criada como NOT NULL. Por fim, na instrução INSERT eu especifiquei apenas as colunas não nuláveis (obrigatórias).

Nulo não é valor, pelo contrário, nulo indica ausência de valor.

Constraints

Como já foi mencionado, constraint é um mecanismo que permite restringir o conjunto de valores para uma dada coluna ou, no nível da tabela, como os valores de diferentes colunas estão relacionados (relacionamento entre colunas da mesma tabela ou entre colunas de tabelas diferentes). Os tipos de constraints são:

Default Values (Valor Padrão) Integridade de domínio (coluna)
Check (Checagem / Verificação) Integridade de domínio (coluna)
Primary Key (chave-primária) Integridade de entidade (linha)
UNIQUE KEY (chave-única) Integridade de entidade (linha)
Foreign Key (chave-estrangeira) Integridade referencial (entre colunas ou tabelas)

DEFAULT Constraints

Este tipo de constraint fornece um valor padrão para uma coluna. Ter um valor padrão é um recurso útil nas instruções de INSERT. Vejamos abaixo.

USE DBExame70461;

DROP TABLE dbo.Produto;

CREATE TABLE dbo.Produto
(    Id INT PRIMARY KEY DEFAULT 1,
    Numero INT DEFAULT 77,
    Nome VARCHAR(50) NOT NULL DEFAULT 'Produto geral',
    Descricao VARCHAR(500) NULL DEFAULT 'Descrição geral'
);
GO

INSERT dbo.Produto (Id, Numero, Nome, Descricao)
VALUES (DEFAULT, DEFAULT, DEFAULT, DEFAULT);

INSERT dbo.Produto (Id, Numero, Nome, Descricao)
VALUES (2, 88, 'Batata', DEFAULT);

INSERT dbo.Produto (Id)
VALUES (3);

Note que destruí e recriei a tabela. Para criar a constraint bastou adicionar a palavra reservada DEFAULT na instrução CREATE TABLE. No comando INSERT usei a palavra DEFAULT para que seja gravado o valor padrão na respectiva coluna. Na terceira instrução INSERT eu especifiquei apenas a coluna ID, omitindo as demais. Isso é válido quando as colunas omitidas são nuláveis ou possuem constraint default.

CHECK Constraints

Este tipo de constraint limita os valores que são permitidos em uma coluna. Vamos explorar algumas possibilidades.

CREATE TABLE dbo.Vendedores
(    Id INT NOT NULL PRIMARY KEY,
    Nome varchar(50) NOT NULL,
    Sexo char(1) NULL CHECK (Sexo in('M','F')) DEFAULT 'M',
    Comissao int NOT NULL
        constraint Ck_Vendedores_Comissao
        CHECK (Comissao BETWEEN 1 AND 5),
    TipoDeSalario varchar(10) NOT NULL
        CHECK (TipoDeSalario in ('Hora','Semana','Mes')),
    Telefone varchar(9) NULL CHECK (LEN(Telefone) >= 8)
);

Para criar a constraint bastou utilizar a palavra reservada CHECK na instrução CREATE TABLE. Na coluna COMISSAO, de forma explícita, informei o nome para a constraint: CK_Vendedores_Comissao. O benefício de criar uma constraint check com nome customizado é a facilidade em futuras alterações usando o comando ALTER TABLE. Note que para a coluna TELEFONE foi definida uma constraint que obriga o usuário a informar um número de telefone com pelo menos oito dígitos. Perceba, ainda, que para a coluna SEXO criei duas constraints: uma CHECK e outra DEFAULT. A figura abaixo mostra como ficou a tabela após a sua criação.

image

Vejamos o que acontece quando se tenta inserir, por exemplo, um registro de vendedor com uma comissão maior que 5% e com um número de telefone de 3 dígitos.

-- Tentando inserir registro com comissão acima de 5%
INSERT dbo.Vendedores (Id, Nome, Sexo, Comissao, TipoDeSalario, Telefone)
VALUES (1, 'Antonio Silva', 'M', 7, 'Mes', 123456789);

image

-- Tentando inserir registro com telefone de 3 dígitos
INSERT dbo.Vendedores (Id, Nome, Sexo, Comissao, TipoDeSalario, Telefone)
VALUES (1, 'Antonio Silva', 'M', 5, 'Mes', 123);

image

Agora, vamos falar de algo perigoso. Veja que definimos a coluna SEXO como nulável. Contudo, a constraint CHECK nessa coluna obriga informar apenas os valores M e F. Mas, o que acontecerá se tentarmos inserir um NULL nesta coluna? Devido ao fato da coluna aceitar NULL então a constraint não considera um NULL como falso, ou, em outras palavras, o NULL é simplesmente ignorado.

-- Inserindo com sucesso um NULL na coluna SEXO 
INSERT dbo.Vendedores (Id, Nome, Sexo, Comissao, TipoDeSalario, Telefone)
VALUES (1, 'Antonio Silva', NULL, 1, 'Mes', 123456789);

Imaginemos um cenário em que tenhamos de importar um arquivo texto contendo um cadastro de vendedores. Ao abrir o arquivo texto notamos que foi informado valores de comissão fora da faixa de 1 a 5 (existem comissões zeradas e comissões maiores que 5). Para fazer a importação será necessário desabilitar a constraint na coluna COMISSAO e, após a importação, reabilitá-la.

-- Desabilitando a constraint check da coluna COMISSAO
ALTER TABLE dbo.Vendedores
    NOCHECK CONSTRAINT CK_Vendedores_Comissao;

-- Inserindo valores fora da faixa de 1 a 5
INSERT dbo.Vendedores (Id, Nome, Sexo, Comissao, TipoDeSalario, Telefone)
VALUES 
(2, 'Jose', 'M', 7, 'Mes', 123456789),
(3, 'Pedro', 'M', 0, 'Mes', 123456789);

-- Habilitando a constraint check da coluna COMISSAO
ALTER TABLE dbo.Vendedores
    CHECK CONSTRAINT CK_Vendedores_Comissao;

Para eliminar uma constraint, independentemente do seu tipo (seja CHECK, DEFAULT, PRIMARY KEY, etc), faça conforme abaixo.

ALTER TABLE dbo.Vendedores
    DROP CONSTRAINT CK_Vendedores_Comissao;

Imaginemos agora o seguinte cenário (absurdo diga-se de passagem): alterar a tabela e adicionar uma constraint que não permita cadastrar, para novas inserções, nomes de vendedores contendo a palavra SILVA. Atente para o fato de que a nossa tabela de VENDEDORES já contém uma pessoa com o sobrenome SILVA. Veja:

image

Vamos tentar criar essa constraint e ver o que acontece.

-- Tentando criar uma constraint na coluna NOME
ALTER TABLE dbo.Vendedores
    ADD CONSTRAINT CK_Vendedores_Nome CHECK (Nome NOT LIKE '%Silva%');

O SQL Server retorna o seguinte erro:

Msg 547, Level 16, State 0, Line 1

The ALTER TABLE statement conflicted with the CHECK constraint “CK_Vendedores_Nome”. The conflict occurred in database “DBExame70461”, table “dbo.Vendedores”, column ‘Nome’.

Isso acontece porque no momento da criação da constraint o SQL Server verifica, por padrão, se existe algum valor na coluna que não atenda à regra especificada. Como o nosso cenário diz que essa regra deva ser aplicada apenas para “novas inserções”, vejamos como contornar tal situação.

ALTER TABLE dbo.Vendedores WITH NOCHECK
    ADD CONSTRAINT CK_Vendedores_Nome CHECK (Nome NOT LIKE '%Silva%');

Usamos a opção WITH NOCHECK, a qual instrui o SQL Server a criar a constraint sem fazer qualquer verificação. Desta forma, a constraint terá efeito somente para novas inserções. Exemplo:

INSERT dbo.Vendedores (Id, Nome, Sexo, Comissao, TipoDeSalario, Telefone)
VALUES (4, 'Bruno Silva', 'M', 2, 'Mes', 123456789);

image

Table-Level CHECK Constraints

Além de checar valores para uma coluna em particular, uma constraint CHECK pode também ser aplicada no nível da tabela para checar valores relacionados em mais de uma coluna (desde que sejam da mesma tabela). Segue exemplo.

CREATE TABLE dbo.Ferias
(    Id INT NOT NULL PRIMARY KEY,
    Funcionario VARCHAR(50) NOT NULL,
    DataDeInicio DATETIME2 NOT NULL,
    DataDeFim DATETIME2 NOT NULL,
    CONSTRAINT CK_Ferias_Periodo CHECK (DataDeInicio < DataDeFim)
);

-- Tentando inserir uma Data de Início inválida
INSERT dbo.Ferias (Id, Funcionario, DataDeInicio, DataDeFim)
VALUES (1, 'Mariana Souza', '2012-10-31', '2012-10-30');

image

Veja que a constraint check força a integridade, garantindo que a data de início não seja igual ou maior que a data de fim.

Conclusão

No próximo post vamos finalizar o assunto de constraints, abordando os tipos primary key, unique key e foreign key. Até lá!


[SQL Server] Exame 70-461–Material para Complementar os Estudos

Devido ao fato do exame 70-461 ser novo (oficialmente disponível desde o mês de julho deste ano), praticamente não existem materiais de referência em português. Todavia, temos à disposição boas fontes de estudo que estão em inglês.

A primeira referência que deixo para vocês são os dois livros citados abaixo:

Livro: Microsoft SQL Server 2012 T-SQL Fundamentals
Autor: Itzik Ben-Gan
Link:
http://www.amazon.com/Microsoft-Server-2012-T-SQL-Fundamentals/dp/0735658145
Meus comentários: este livro cobre todos os tópicos exigidos no exame 70-461 e vai um pouco além. Ele te ajudará a dominar os fundamentos da programação T-SQL, proporcionando uma excelente base de conhecimentos para os exames seguintes também. Recomendo fortemente.

Livro: Training Kit Exam 70-461: Querying Microsoft SQL Server 2012
Autor: Itzik Ben-Gan
Link:
http://www.amazon.com/Training-Kit-Exam-70-461-Microsoft/dp/0735666059
Meus comentários: sem dúvida alguma a série Training Kit é a que melhor direciona os estudos, pois aborda com exatidão os assuntos exigidos no exame. Além de exercícios que reforçam a aprendizagem, o livro vem com um CD contendo testes que simulam o exame. Vale à pena.

 

Para aqueles que gostam de estudar através da internet, estou deixando uma listagem dos assuntos do exame + respectivos links para os estudos.

1) Create Database Objects (24%)

Create and alter tables using T-SQL syntax (simple statements)

CREATE TABLE

Microsoft Links:
http://msdn.microsoft.com/en-us/library/ms174979(v=sql.110).aspx

DROP TABLE

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms173790(v=sql.110).aspx

ALTER TABLE

Microsoft Links:

http://msdn.microsoft.com/en-us/library/ms190273(v=sql.110).aspx

ALTER COLUMN

Microsoft links:

Veja Alter table

Create and alter views (simple statements)

CREATE VIEW

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms187956(v=sql.110).aspx

ALTER VIEW

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms173846(v=sql.110).aspx

DROP VIEW

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms173492(v=sql.110).aspx

Create indexed views

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms191432(v=sql.110).aspx

Create and modify constraints (simple statements)

create constraints on tables

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms188066(v=sql.110).aspx

define constraints

Microsoft links:

Veja acima

unique constraints

Microsoft links:

Veja acima

default constraints

Microsoft links:

Veja acima

primary and foreign key constraints

Microsoft links:

Veja acima

Create and alter DML triggers

inserted and deleted tables

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms189799(v=sql.110).aspx

http://msdn.microsoft.com/en-us/library/ms176072(v=sql.110).aspx

nested triggers

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms190739(v=sql.110).aspx

types of triggers

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms178110(v=sql.110).aspx

handle multiple rows in a session

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms190752(v=sql.110).aspx

 

2) Work with Data (27%)

Query data by using SELECT statements

use the ranking function to select top(X) rows for multiple categories in a single query

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms189798(v=sql.110).aspx

write and perform queries efficiently using the new code items such as synonyms and joins (except, intersect)

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms188055(v=sql.105).aspx

Implement sub-queries

pivot and unpivot

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms177410(v=SQL.105).aspx

apply operator

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms175156(v=SQL.105).aspx

cte statement

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms190766(v=SQL.105).aspx

with statement

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms175972(v=sql.110).aspx

Implement data types

impact of GUID (newid, newsequentialid) on database performance

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms190348.aspx

http://msdn.microsoft.com/en-us/library/ms189786(v=sql.110).aspx

Implement aggregate queries

new analytic functions

Microsoft links:

http://msdn.microsoft.com/en-us/library/hh213234

spatial aggregates

Microsoft links:

http://msdn.microsoft.com/en-us/library/hh403400(v=sql.110).aspx

Query and manage XML data

understand XML datatypes and their schemas and interoperability with limitations and restrictions

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms189887(v=SQL.90).aspx

implement XML schemas and handling of XML data

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms176009(v=sql.110).aspx

how to handle XML data in SQL Server and when and when not to use it, including XML namespaces

Microsoft links:

http://msdn.microsoft.com/en-us/library/bb522446(v=sql.110).aspx

import and export XML

Microsoft links:

http://msdn.microsoft.com/en-us/library/aa179112(v=SQL.80).aspx

http://msdn.microsoft.com/en-us/library/aa179108(v=sql.110).aspx

XML indexing

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms191497(v=sql.110).aspx

 

3) Modify Data (24%)

Create and alter stored procedures (simple statements)

write a stored procedure to meet a given set of requirements

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms190782(v=sql.110).aspx

branching logic

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms182717(v=sql.110).aspx

create stored procedures and other programmatic objects

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms182717(v=sql.110).aspx

create stored procedures and other programmatic objects

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms187926(v=sql.110).aspx

Modify data by using INSERT, UPDATE, and DELETE statements

given a set of code with defaults, constraints, and triggers, determine the output of a set of DDL

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms174335(v=sql.110).aspx

http://msdn.microsoft.com/en-us/library/ms177523(v=sql.110).aspx

http://msdn.microsoft.com/en-us/library/ms189835(v=sql.110).aspx

use output statement

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms177564(v=sql.110).aspx

Combine datasets

difference between UNION and UNION all

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms180026(v=sql.110).aspx

modify data by using MERGE statements

Microsoft links:

http://msdn.microsoft.com/en-us/library/bb510625(v=sql.110).aspx

Work with functions

understand deterministic and non-deterministic functions

Microsoft links:

http://msdn.microsoft.com/en-us/library/aa214775(v=SQL.110).aspx

create and alter user-defined functions (UDFs)

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms186755(v=sql.110).aspx

http://msdn.microsoft.com/en-us/library/ms186967(v=sql.110).aspx

4) Troubleshoot and Optimize Queries (25%)

Optimize queries

understand statistics

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms190397(v=sql.110).aspx

DMVs

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms188754(v=sql.110).aspx

hints

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms181714(v=sql.110).aspx

statistics IO

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms184361(v=sql.110).aspx

Manage transactions

understand begin tran, commit, and rollback

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms174377(v=sql.110).aspx

http://msdn.microsoft.com/en-us/library/ms188929(v=sql.110).aspx

http://msdn.microsoft.com/en-us/library/ms190295(v=sql.110).aspx

http://msdn.microsoft.com/en-us/library/ms181299(v=sql.110).aspx

implicit vs. explicit transactions

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms187807(v=sql.110).aspx

isolation levels

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms173763.aspx

trancount

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms187967(v=sql.110).aspx

Implement error handling

implement try/catch/throw

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms175976(v=sql.110).aspx

transaction management

Microsoft links:

http://msdn.microsoft.com/en-us/library/ms174377(v=sql.110).aspx


[SQL Server] Exame 70-461 – Tópico 3: Projetando Views – Parte 2 – Final

[SQL Server] Exame 70-461 – Tópico 3: Projetando Views – Parte 2 – Final

Dando sequência aos estudos do tópico Projetando Views, vamos abordar o assunto Implicações de Segurança.

Implicações de Segurança

Um dos benefícios ao se utilizar views é a camada de abstração de segurança que nos é proporcionada. Um usuário pode receber um grant de select em uma view, sem a necessidade de receber o mesmo grant de select nas tabelas base referenciadas. Isso significa que o usuário consegue fazer select na view, mas não consegue fazer select nas tabelas usadas pela view. Além disso, views é uma forma de implementar segurança tanto em coluna quanto em linha. É possível no SQL Server dar grant’s em colunas específicas de determinada tabela, mas não diretamente nas linhas. Imagine um cenário em que o DBA é requisitado a configurar as seguintes permissões para o usuário Pedro:

A) Visualizar e ler o conteúdo das colunas Código, Nome e CPF da tabela Cliente;

B) Ler apenas as linhas cuja coluna SALARIO seja inferior a R$ 1.000,00.

O primeiro requisito é fácil de ser atendido e, para isso, o grant de select poderia ser setado diretamente nas colunas da tabela. Contudo, para o segundo requisito não existe um “tipo de grant” e, desta forma, precisamos recorrer a uma abordagem que implemente ambos os requisitos numa única “tacada”. Vejamos como.

 

use DBExame70461
go

— Se a tabela Cliente existir então vamos dropá-la
IF OBJECT_ID(‘dbo.Cliente’) IS NOT NULL
DROP TABLE dbo.Cliente;

— Criando a tabela CLIENTE
CREATE TABLE dbo.Cliente
( Codigo INT NOT NULL IDENTITY PRIMARY KEY,
Nome varchar(50) NOT NULL,
Sexo CHAR(1) NOT NULL,
Telefone CHAR(8) NOT NULL,
CPF CHAR(11) NOT NULL,
SALARIO DECIMAL(10,2) NOT NULL);
GO

— Inserindo duas linhas
INSERT INTO dbo.Cliente (Nome, Sexo, Telefone, CPF, Salario)
VALUES (‘Maria’, ‘F’, ‘12345678’, ‘00000000000’, 560.50);

INSERT INTO dbo.Cliente (Nome, Sexo, Telefone, CPF, Salario)
VALUES (‘Joana’, ‘F’, ‘12345678’, ‘11111111111’, 2300.44);
GO

IF OBJECT_ID(‘dbo.vwClientes’) IS NOT NULL
DROP VIEW dbo.vwClientes;
GO

— Criando a view
CREATE VIEW dbo.vwClientes
AS
SELECT c.Codigo, c.Nome, c.CPF
FROM dbo.Cliente c
WHERE c.SALARIO < 1000.00
GO

 

Até neste ponto temos o nosso cenário construído. Agora vamos criar um usuário de banco e conceder grant de select sobre a view. Não se preocupem especificamente com as instruções de criação e gerenciamento de usuário, pois as mesmas estão fora do escopo do exame 70-461.

 

— Criando um usuário de banco de nome Pedro
CREATE USER Pedro WITHOUT LOGIN;

— Concedendo o GRANT de SELECT para Pedro
GRANT SELECT ON dbo.vwClientes TO Pedro;

 

/*
Neste momento estou alterando o contexto de segurança da sessão e
assumindo a identidade do usuário Pedro
*/
EXECUTE AS USER = ‘Pedro’;

 

/*
Veja que Pedro não consegue fazer um select
diretamente na tabela Cliente
*/
SELECT * FROM dbo.Cliente;

image

 

— Pedro consegue consultar a view vwClientes
SELECT * FROM dbo.vwClientes;

image

Conclusão

No próximo post estarei quebrando a sequência de tópicos do exame para atender a um pedido dos leitores que vêm acompanhando esta série: apresentar uma lista de material de estudo complementar. Vou elencar os tópicos e apontar fontes de estudo para cada um. Até lá.


[SQL Server] Exame 70-461 – Tópico 3: Projetando Views – Parte 1

Este tópico possui um enunciado um tanto confuso: ensure code non regression by keeping consistent signature for procedure, views and function (interfaces); security implications. Inicialmente precisamos perceber que o tópico vai além de views, abordando também procedures e functions. Por conseguinte, vale destacar que o conteúdo a ser estudado para o exame 70-461 possui escopo mais amplo que DML, envolvendo temas como objetos programáveis (views, sp, functions, triggers) e assuntos avançados como transações, manipulação de XML, dentre outros. Assim, não se iluda: estude e se prepare para uma prova desafiadora que lhe exigirá além do conhecimento de consultas T-SQL e modificação de dados.

Quanto ao conteúdo do tópico 3 vamos quebrá-lo em duas partes, visando dirimir quaisquer confusões:
1) Não regressão de código mantendo consistentes assinaturas nas interfaces dos objetos (view, sp, function)
2) Implicações de segurança

Não regressão de código mantendo consistentes assinaturas nas interfaces dos objetos (view, sp, function)

Pode até parecer confuso, mas a idéia por trás dessas palavras é bem simples: quebrar a dependência. Imaginemos uma view que retorna 3 colunas: CODIGO, NOME, DT_NASCIMENTO, sendo todas recuperadas a partir de uma tabela de candidatos a ofertas de emprego. Agora imagine que o DBA renomeou na tabela (e também na view) a coluna DT_NASCIMENTO para DATA_NASCIMENTO. Enquanto não forem recompiladas, as aplicações que utilizam a view passarão a apresentar erro, vez que ainda referenciam a coluna como DT_NASCIMENTO. Este é um problema típico de dependência: os nomes expostos pela view para as colunas são exatamente os mesmos utilizados nas tabelas. Sendo assim, tudo o que precisamos fazer é tratar a assinatura do objeto, expondo nomes que serão mantidos independente de quaisquer modificações nas tabelas referenciadas, de modo a garantir a não necessidade de manutenção nas aplicações ou em outros objetos programáveis do BD que consomem a view. Vejamos o exemplo da tabela CANDIDATO.

USE DBExame70461
GO
— Cria a tabela de candidatos
CREATE TABLE dbo.Candidato
( CODIGO INT,
NOME VARCHAR(50),
DT_NASCIMENTO DATETIME);
GO

INSERT dbo.Candidato VALUES(1, ‘Mariana Pereira da Silva’, ‘19801224’);
GO

— Cria a view
CREATE VIEW dbo.vwCandidatos
AS
SELECT Codigo, Nome, Dt_Nascimento
FROM dbo.Candidato;
go

— Cria uma sp para consumir a view
CREATE PROCEDURE dbo.uspObterCandidatos
AS
SELECT Codigo, Nome, Dt_Nascimento FROM dbo.vwCandidatos;
GO

Neste exemplo criamos uma procedure que consome a view. Note que esta procedure produz um result set, ou seja, apresenta o resultado de uma query. A SP (stored procedure) tem uma dependência direta com a view, enquanto que esta usa os mesmos nomes de colunas da tabela referenciada. Para executarmos a SP basta fazer como abaixo:

EXEC dbo.uspObterCandidatos;

Agora vamos renomear a coluna DT_NASCIMENTO para DATA_NASCIMENTO, na tabela.

— Renomeando a coluna na tabela
EXEC sp_rename ‘dbo.Candidato.DT_NASCIMENTO’, ‘DATA_NASCIMENTO’;

Observe a mensagem que aparece após a execução desta sp builtin: “Caution: Changing any part of an object name could break scripts and stored procedures”. Neste momento a view não está mais funcionando, sendo necessário alterar a view.

— Renomeando na view
ALTER VIEW dbo.vwCandidatos
AS
SELECT Codigo, Nome, Data_Nascimento
FROM dbo.Candidato;
go

Como consequência a SP também parou de funcionar, sendo necessário recompilá-la de modo a refletir o novo nome da coluna. Tente executar o código abaixo e veja o resultado:

exec dbo.uspObterCandidatos
Invalid column name ‘Dt_Nascimento’

Para evitar essa “regressão de código”, vamos modificar a assinatura da view e quebrar a dependência (pelo menos no nível da interface). Isso pode ser feito de duas maneiras.

— Criando ALIAS para as colunas diretamente na lista do SELECT
ALTER VIEW dbo.vwCandidatos
AS
SELECT
Codigo As Codigo,
Nome as Nome,
Data_Nascimento As Dt_Nascimento
FROM dbo.Candidato;
go

— Declarando externamente os ALIAS para as colunas
ALTER VIEW dbo.vwCandidatos (Codigo, Nome, Dt_Nascimento)
AS
SELECT
Codigo,
Nome,
Data_Nascimento
FROM dbo.Candidato;
go

Execute novamente a sp abaixo e veja que o problema foi resolvido.
exec dbo.uspObterCandidatos

A adoção deste tipo de abordagem diminui intervenções em aplicações e ou objetos que consomem uma view. A mesma estratégia pode ser aplicada em SP’s que retornem  result set ou em TVF’s – Table Valued Functions (funções que retornam tabelas).Ainda vale destacar o uso da opção SCHEMABINDING. Ao usarmos tal opção o DBA simplesmente não conseguiria alterar a estrutura da tabela, vez que a opção SCHEMABINDING “prende” a tabela à view ou a function (não existe opção SCHEMABINDING em SP), produzindo uma dependência que não pode ser quebrada. Neste caso, o DBA precisaria alterar a view/function e remover a opção SCHEMABINDING para, em seguida, alterar a tabela.

Conclusão
No próximo post falaremos a respeito das considerações de segurança no projeto de views. Até lá.


[SQL Server] Exame 70 461 – Tópico 2: Criando e Alterando Views – Parte 4 (final)

Neste artigo vamos abordar um tipo especial de view conhecida como View Indexada (note a palavra Indexada – ela faz toda a diferença).

Introdução a Índices

Antes de entrarmos no assunto de views indexadas é necessário conhecermos um pouco sobre índices. No BOL – Books Online (o help do SQL Server) encontramos a seguinte definição para índice: é uma estrutura associada com uma tabela ou view que acelera a recuperação das linhas da tabela ou view. Portanto, um índice é algo que acelera nossos SELECT’s, sendo de vital importância para o desempenho das bases de dados. Uma maneira muito simples, e interessante, para entendermos como um índice funciona é fazermos uma correlação com um livro. Imaginemos que você tem em mãos um catálogo telefônico e que deseja localizar a página onde se encontra o nome José da Silva. Você tem duas maneiras de fazer isso: a primeira é ir varrendo as páginas do catálogo, com o esforço de olhar página por página até encontrar o nome José da Silva; a segunda é consultar o índice disponível no início do catálogo e, a partir da informação contida no índice, ir diretamente para a página onde se encontram os nomes que começam com a letra J. É evidente que a segunda maneira é mais rápida! Quando uma tabela possui índices o SQL Server pode então utilizá-los para atender mais rapidamente as consultas.

No SQL Server existe os seguintes tipos de índices:

· Clustered

· Nonclustered

· XML

· Spatial

· Full-Text Search

· Column Store (introduzido no SQL Server 2012)

Alguns autores costumam citar, ainda, índices filtrados como um tipo de índice, mas, pessoalmente, prefiro considerar isso apenas como uma opção para índices Clustered ou Nonclustered. ATENÇÃO: vale destacar que o assunto de Índices não é cobrado diretamente pela grade de conteúdos da prova 70-461, mas deter o mínimo de conhecimento sobre esse assunto poderá ajudar na resolução de algumas questões. Para o tópico que estamos focando neste post, bastará entendermos o que são índices Clustered e Nonclustered.

Quando criamos um índice clustered em uma determinada tabela então as linhas de dados, daquela tabela, são fisicamente armazenadas e ordenadas conforme a chave que compõe o índice. Nota: vale esclarecer que a chave de um índice é constituída de uma ou mais colunas da tabela. Por exemplo, a chave do índice XPTA é a coluna CPF da tabela PESSOA; a chave do índice XPTO são as colunas CNPJ e DATA_COMPRA da tabela COMPRA. Como o índice clustered afeta a ordem física em que as linhas de dados são armazenadas, então só é possível termos um índice clustered por tabela. Uma tabela que possui índice clustered é conhecida como tabela clusterizada. Uma tabela sem índice clustered é conhecida como tabela heap (Heap Table).

Um índice nonclustered não afeta a ordem física em que as linhas da tabela são armazenadas. Ele é uma estrutura auxiliar, separada das linhas de dados da tabela. Na versão 2012 do SQL Server é possível criarmos até 999 índices Nonclustered (não recomendo ninguém cometer tamanha loucura em ambientes OLTP!).

Segue a sintaxe resumida para criação de índices Clustered / Nonclustered:

CREATE [UNIQUE] NONCLUSTERED INDEX NomeDoIndice
ON TabelaXYZ (ColunaA, ColunaB);

CREATE [UNIQUE] CLUSTERED INDEX NomeDoIndice
ON TabelaXYZ (ColunaA);

A expressão UNIQUE, que está entre colchetes e que é opcional, indica se o índice é único, ou seja, se o valor da chave pode ou não ser repetido no decorrer das linhas. Quando UNIQUE é omitido durante a criação então o índice não tem valor exclusivo para a chave. Perceba que a diferença na sintaxe é basicamente o uso das palavras Clustered e Nonclustered.

Views Indexadas (Indexed Views)

Como já foi comentado anteriormente, é possível criarmos índices em views, apesar de o mais usual ser a criação de índices em tabelas. Contudo, ao contrário das tabelas, o primeiro índice a ser criado obrigatoriamente em uma view é um índice do tipo Unique Clustered e a partir daí, opcionalmente, criarmos índices Nonclustered conforme necessidade.

Uma pergunta para testar a sua leitura até aqui: é possível criar um índice Nonclustered em uma view que ainda não possua índice Clustered? NÃO, não é possível.

Quando criamos um índice Unique Clustered sobre uma view o SQL Server irá “materializar” aquela view. Neste caso, o SQL Server cria uma estrutura interna que armazena o resultado (result set) da view, exatamente como se fosse uma tabela com índice clustered. Você pode pensar nessa estrutura como um tipo especial de tabela cujo esquema (colunas) é definido pela instrução SELECT da view. Uma view que possui índice Unique Clustered é conhecida como View Indexada (em outros SGDBs você encontra conceito equivalente como Views Materializadas).

Mas por que usar views indexadas? Quais os benefícios? Imagine aquela view com vários JOIN’S e agregações, referenciando tabelas de vários milhões de linhas. Uma view indexada pode, nesse cenário, acelerar, e muito, as consultas, pois, como os dados já estão persistidos numa estrutura separada, o SQL Server não precisará acessar várias tabelas, aplicar várias agregações, etc. Pelo contrário, bastará ao SQL Server resgatar as linhas diretamente na view indexada. Evidentemente que não existe almoço grátis. Existe um trade-off que precisa ser considerado antes de sairmos por aí criando views indexadas. É necessário balancear o benefício que teremos ao acelerar as consultas contra o custo na modificação de dados. Uma view indexada causa impacto sobre as modificações de dados nas tabelas referenciadas pela view. Toda vez que fizermos um INSERT / UPDATE / DELETE em alguma tabela que é referenciada por uma view indexada, o SQL Servir precisará também replicar a modificação na view indexada. Assim, um INSERT, por exemplo, acontecerá em dois lugares: uma vez na tabela e outra na view indexada.

Criação

Na criação de views indexadas é obrigatório que:

· Usemos a opção WITH SCHEMABINDING;

· Criemos o índice clustered como unique.

Agora vamos alterar a view vwProdutos, criada em post anterior desta série, tornando-a numa view indexada. O primeiro passo é alterar a view e incluir a opção SCHEMABINDING.

USE DBExame70461
GO

ALTER VIEW vwProdutos
WITH SCHEMABINDING
AS

SELECT p.ID , p.Nome AS Produto, p.CategoriaID
FROM dbo.Produto p
where p.CategoriaID = 2
GO

Agora vamos criar um unique índice clustered.

create unique clustered index vwProdutos_ID
ON dbo.vwProdutos (ID);

Veja que o índice é unique. É muito importante não esquecer esse detalhe.

Conclusão

A criação de views indexadas é simples, vez que basta incluir a opção SCHEMABINDING na sintaxe e criar um índice unique clustered na view. É vital estarmos atentos a esses detalhes, pois é quase certo que você encontre na prova 70-461 pelo menos uma questão abordando views indexadas.


[SQL Server] Exame 70 461 – Tópico 2: Criando e alterando views – Parte 3

Views Atualizáveis (Updatable Views)

É possível através de uma view fazermos modificações nos dados da tabela base referenciada. Em outras palavras, podemos executar instruções UPDATE, DELETE e INSERT diretamente contra a view. Este recurso aumenta a granularidade da segurança, algo que estaremos abordando em tópico futuro. Contudo, executar modificações de dados diretamente sobre uma view é algo que possui algumas restrições:

· A modificação não pode afetar mais que uma tabela base: ou a view, em sua instrução SELECT, referencia apenas uma única tabela, ou a view possui uma trigger do tipo instead of;

· A modificação não pode afetar colunas agregadas;

· A modificação precisa preencher as colunas NOT NULL da tabela base.

Analisemos um exemplo.

use DBExame70461

go

— 1) Inicialmente criar duas tabelas: Categoria e Produto.

CREATE TABLE Categoria

( ID INT NOT NULL PRIMARY KEY,

Descricao VARCHAR(50) NOT NULL

);

CREATE TABLE Produto

( ID INT NOT NULL PRIMARY KEY,

CategoriaID INT NOT NULL,

Nome VARCHAR(50) NOT NULL,

Preco DECIMAL(15,2) NULL

);

— 2) Popular com alguns valores.

INSERT Categoria VALUES(1,’Frutas’),(2,’Carros’);

INSERT INTO Produto (ID,CategoriaID,Nome,Preco)

VALUES

(1,1,’Maçã’,null),(2,1,’Abacaxi’,2.5),

(3,2,’Tucson’,70000),(4,2,’Fiat Uno’,null);

GO

Vamos agora criar uma view que referencia essas duas novas tabelas e, em seguida, realizar uma operação de DELETE (ou pelo menos tentar).

— Cria a view

CREATE VIEW vwProdutos

AS

SELECT

p.ID

, p.Nome AS Produto — NOTE o uso de ALIAS para a coluna Nome

, c.Descricao AS Categoria — NOTE o uso de ALIAS para a coluna Descricao

, p.Preco

FROM dbo.Produto p

JOIN dbo.Categoria c ON p.CategoriaID = c.ID

GO

— Executando a view e conferindo os resultados

SELECT * FROM vwProdutos;

image

Agora vamos tentar excluir o registro de ID = 4 (Fiat Uno).

— Deletando o Fiat Uno…???

DELETE FROM vwProdutos WHERE ID = 4;

image

O SQL Server retornou erro informando que a view não é atualizável porque referencia múltiplas tabelas. Desta forma, não importa se a operação é INSERT, UPDATE ou DELETE.

Vamos alterar a definição da view, fazendo com que referencie apenas a tabela Produto. Iremos, inclusive, remover a coluna Preco (lembre-se que essa coluna não é obrigatória) e, portanto, a view será ainda atualizável.

— Alterando a definição da view

— Removeremos a coluna Preco que não é obrigatória

— Incluiremos a coluna CategoriaID que é obrigatória

— Esta modificação torna a view atualizável

ALTER VIEW vwProdutos

AS

SELECT

p.ID

, p.Nome AS Produto

, p.CategoriaID

FROM dbo.Produto p

GO

Vejamos o que acontece quando executamos o script abaixo.

INSERT INTO vwProdutos (ID, Produto, CategoriaID) VALUES (5, ‘Melancia’, 1);

DELETE FROM vwProdutos WHERE ID = 4;

SELECT * FROM vwProdutos;

image

Note que tudo funcionou perfeitamente. Uma nova linha foi inserida e o registro de ID = 4 foi excluído.

CHECK OPTION

Esta é uma parte opcional da sintaxe de criação de views e só tem aplicabilidade quando trabalhamos com views atualizáveis. Quando usamos CHECK OPTION o SQL Server forçará qualquer instrução de modificação de dados, executada contra a view, a seguir o filtro definido na cláusula WHERE do SELECT da view.

Vamos rever a sintaxe para a criação / alteração de views:

CREATE VIEW NomeDaView

[ WITH ENCRYPTION / SCHEMABINDING / VIEW_METADA ]

AS

SELECT * FROM Tabela

[ WITH CHECK OPTION ]

GO

Vale destacar que aquilo que aparece entre colchetes é de uso opcional na sintaxe. Atente para o detalhe de que CHECK OPTION é declarado no final da view. Vamos exemplificar o uso dessa opção, alterando a view que criamos em nossos exemplos anteriores.

ALTER VIEW vwProdutos

AS

SELECT

p.ID

, p.Nome AS Produto

, p.CategoriaID

FROM dbo.Produto p

where p.CategoriaID = 2

WITH CHECK OPTION

GO

Conforme a alteração que fizemos, a partir de agora a view retornará exclusivamente produtos da categoria de código 2. Da mesma forma, o uso de CHECK OPTION permitirá inserir/alterar/deletar somente os produtos dessa categoria.

— Tentando inserir um produto de categoria <> 2

INSERT INTO vwProdutos (ID, Produto, CategoriaID) VALUES (6, ‘Jaca’, 1);

image

Veja que não é possível inserir registro que não atenda ao critério especificado na cláusula WHERE.

— Inserindo um novo veículo

INSERT INTO vwProdutos (ID, Produto, CategoriaID) VALUES (6, ‘FUSCA 77’, 2);

image

Pronto. Registro inserido com sucesso.

Conclusão

No próximo post estarei abordando exclusivamente o assunto views indexadas e, finalmente, concluindo o tópico 2 do exame 70-461. Dentro em breve publicarei um post contendo material complementar para apoiar os estudos. Quando fecharmos o primeiro módulo – Criando Objetos de Banco de dados (Create Database Objects), disponibilizarei um pequeno simulado abordando esse módulo. Não deixe de acompanhar a série.


[SQL Server] Exame 70 461–Tópico 2: Criando e alterando views – Parte 2

Dando sequência ao conteúdo do Tópico 2, vamos explorar outras opções da sintaxe de criação / alteração de Standard Views (views padrões) . Além do SCHEMABINDING, apresentado no post anterior, temos a opção ENCRYPTION, a qual ofusca o código fonte da view. Por exemplo, o código abaixo, sem a opção ENCRYPTION, deixará exposto o script de criação da view para os usuários do banco de dados AdventureWorks2012, que possuam GRANT de VIEW DEFINITION sobre o objeto.

use AdventureWorks2012
GO

CREATE VIEW [dbo].[vwEmpregadosDoSexoMasculino]
AS
SELECT
    p.FirstName AS PrimeiroNome
    , p.LastName AS UltimoNome
    , h.HireDate AS DataContratacao
FROM HumanResources.Employee h
JOIN Person.Person p
    ON h.BusinessEntityID = p.BusinessEntityID
WHERE h.Gender = ‘M’;

Para visualizar o script de criação do objeto os usuários podem recorrer ao SSMS – SQL Server Management Studio, clicando com o botão direito sobre a view, acionando a opção Script View as / CREATE To. Também é possível recuperar o script de criação usando T-SQL, conforme opções abaixo:

— Opções para visualizar o fonte da view
EXEC sys.sp_helptext ‘vwEmpregadosDoSexoMasculino’;

SELECT * from sys.sql_modules s
where s.object_id = object_id(‘vwEmpregadosDoSexoMasculino’);

Agora vamos alterar a view usando ENCRYPTION:

ALTER VIEW [dbo].[vwEmpregadosDoSexoMasculino]
with encryption
AS
SELECT
    p.FirstName AS PrimeiroNome
    , p.LastName AS UltimoNome
    , h.HireDate AS DataContratacao
FROM HumanResources.Employee h
JOIN Person.Person p
    ON h.BusinessEntityID = p.BusinessEntityID
WHERE h.Gender = ‘M’;

Qualquer usuário do banco, e até mesmo membros da server role sysadmin, não conseguirão visualizar ou recuperar o script de criação do objeto. Tente executar o código abaixo e analise o retorno.

EXEC sys.sp_helptext ‘vwEmpregadosDoSexoMasculino’;

Assim, a cláusula WITH ENCRYPTION funciona como um recurso para a proteção da definição da view. Entretanto, atente para o detalhe de que essa opção torna indisponível a ação de recuperar o código fonte até mesmo para o criador do objeto.

Segue algumas considerações quanto ao uso dessa cláusula:

  1. Mantenha cópia do código fonte da view – isto é muito importante quando se usa a cláusula WITH ENCRYPTION;
  2. Tenha em mente de que a opção ENCRYPTION proporciona uma ofuscação limitada – ela não é uma proteção inviolável. Muitos utilitários de terceiros, existentes “por aí”, podem quebrar a criptografia;
  3. E o mais importante (sob minha ótica): encriptar código de objetos programáveis, como views, podem atrapalhar e tornar mais árduo o trabalho de diagnosticar problemas de performance, especialmente quando fazemos trace.

Limitações

Seguem algumas limitações que precisam ser consideradas na criação de standard views:

  1. Máximo de 1024 colunas referenciadas;
  2. Não suporta clásula INTO;
  3. Não pode referenciar tabela temporária e variável de tabela;
  4. Não suporta ORDER BY, exceto se usar TOP. Neste ponto existem alguns fatos interessantes sobre ordenação em views, os quais não são relevantes para o exame 70-461. Contudo, publicarei no futuro um post tratando dessas questões;
  5. Uma instrução CREATE VIEW XXX precisa ser a única existente dentro de um lote (diretiva GO);
  6. SELECT * somente pode ser usado na definição da view se a opção SCHEMABINDING não estiver presente.

As limitações expostas anteriormente tendem a aparecer no exame. Por isso, aprenda. Vejamos alguns casos.

CASO 1 – Avalie se a instrução abaixo é válida.

use AdventureWorks2012
go

CREATE VIEW vwCategorias
with schemabinding
AS
    SELECT * FROM Production.ProductCategory
GO

 

Se você respondeu NÃO então você está correto. A instrução não é válida porque a cláusula WITH SCHEMABINDING não permite o uso do “*”. Para criar a view usando “*” então faça conforme abaixo:

CREATE VIEW vwCategorias
AS
    SELECT * FROM Production.ProductCategory
GO

 

CASO 2 – Identifique o que está errado na instrução abaixo.

use AdventureWorks2012
GO

DROP VIEW vwCategorias;

CREATE VIEW vwProdutos
AS
select p.ProductID, p.Name
from Production.Product p;
go

 

Basta lembrar da limitação “Uma instrução CREATE VIEW XXX precisa ser a única existente dentro de um lote (diretiva GO)”. Vejamos:

image

Desta maneira, a forma correta para o script é:

use AdventureWorks2012
GO

DROP VIEW vwCategorias;
GO

CREATE VIEW vwProdutos
AS
select p.ProductID, p.Name
from Production.Product p;
go

 

CASO 3 – Uso do ORDER BY

Importante memorizar que somente o segundo script é válido na criação de views.

use AdventureWorks2012
GO

— Forma incorreta do uso de ORDER BY
CREATE VIEW vwVendas
AS
    SELECT h.SalesOrderID, h.OrderDate
    FROM Sales.SalesOrderHeader h
    ORDER BY h.OrderDate DESC
GO

— Forma correta do uso de ORDER BY
CREATE VIEW vwVendas
AS
    SELECT TOP 100 PERCENT h.SalesOrderID, h.OrderDate
    FROM Sales.SalesOrderHeader h
    ORDER BY h.OrderDate DESC
GO

 

Uma alternativa, particularmente interessante, é o uso da função de ranqueamento ROW_NUMBER. Veja como:

CREATE VIEW vwVendas
AS
    SELECT
        row_number() over(ORDER BY  h.OrderDate DESC) Numero,
        h.SalesOrderID,
        h.OrderDate
    FROM Sales.SalesOrderHeader h

 

Conclusão

Minha intenção inicial era abordar o “Tópico 2: Criando e alterando views”, escrevendo no máximo 2 posts. Contudo, isso não foi possível, já que existem ainda questões importantíssimas, merecedoras de artigo dedicado, e eu não desejo alongar-me na escrita . Assim, continue acompanhando a série e não perca “as cenas do próximo capítulo”.