Distribuição com PL/Ruby e DbLink no Postgresql
Saturday, January 12th, 2008Depois de uma escravização no fim do ano passado e no inicio deste, estou de volta para “espalhar a maldade”!
Vou falar sobre PL/Ruby ou melhor fazer funcionar.
PL/Ruby é uma linguagem procedural acoplada dentro do Postgresql. Entre as linguagem do Postgres é a mais flexível e fácil.
Demonstrarei inicialmente “o jeitão da coisa” e depois uma implementação distribuída.
Instalação
Instale o banco e o módulo plruby, se o postres já estiver instalado, instale apenas o módulo(claro!)
apt-get install postgresql-8.1 postgresql-8.1-plruby
Agora conecte via psql
psql template1
Criaremos um banco novo para todos os testes deste tutorial, o banco “ruby_test”, então vamos lá
template1=# create database ruby_test;
conecte no banco novo
template1=# \c ruby_test
Crie a função/linguagem via o script plruby.sql que faz parte do pacote postgresql-8.1-plruby, se a sua distro difere de ubuntu, verifique onde o arquivo plruby.sql está e invoque com \i semelhante ao exemplo abaixo.
ruby_test=# \i /usr/share/postgresql-8.1-plruby/plruby.sql
Jeitão
Para familiarizarmos um pouco vamos criar uma função que produz uma seqüencia de números separados por espaço.
Portanto a função abaixo cria um Range do primeiro argumento(args[0]) até o segundo argumento(args[1]), converte o range para Array e depois para String separando os elementos por espaço.
create or replace function string_array(int,int) returns text as $RUBY$ (args[0]..args[1]).to_a.join " " $RUBY$ language plruby;
Executando
ruby_test=# select string_array(10,20);
Produzindo
"10 11 12 13 14 15 16 17 18 19 20"
Agora uma função que procura um padrão dentro do um texto, se o padrão for encontrado, o mesmo ficará entre tags <b>.
create or replace function find(varchar,text) returns text as $RUBY$ args[1].gsub(/(#{args[0]})/,'<b>\1</b>') $RUBY$ language plruby;
ruby_test=# select find('[tT]he','The method PL#context and PL#context= give the possibility to store information between the call')
Produto
"<b>The</b> method PL#context and PL#context= give <b>the</b> possibility to store information between <b>the</b> call"
Até agora demonstramos duas funções, faz uma lógica e retorna apenas uma tupla.
Ficou bem elegante essa implementação do PL/Ruby para vários registros(setof) sendo que cada registro é um objeto lançado pelo operador yield do Ruby.
Uma função em PL/Ruby para resolver a função(x ao quadrado vezes a raíz de 2)
f(x) = x^2 * raiz(2)
sendo x inteiro variando de x1 a x2.
create or replace function fx(int,int) returns setof float as $RUBY$ (args[0]..args[1]).each {|x| yield x.to_i**2 * Math.sqrt(2) } $RUBY$ language plruby;
Executando com * from pois estamos pegando vários registros
ruby_test=# select * from fx(1,10);
Resultando em
1.4142135623731 5.65685424949238 12.7279220613579 22.6274169979695 35.3553390593274 50.9116882454314 69.2964645562817 90.5096679918781 114.551298552221 141.42135623731
Manipulando dados
Vamos criar uma tabela denominada “users” para povoarmos de dados para os testes.
ruby_test=# CREATE TABLE users( id serial not null primary key, name varchar(255) not null , login varchar(80), email varchar(80), password varchar(80) ) ;
Inserindo alguns registros
ruby_test=# insert into users(name,login,email,password) values ('Fulano de tal','fulano','fulano@server.com','fulanopwd');
ruby_test=# insert into users(name,login,email,password) values ('Maria da Silva','maria','maria.silva@server.com','mariapwd');
ruby_test=# insert into users(name,login,email,password) values ('Sem nome','semnome','semnome@server.com','semnomepwd');
Daqui a pouco faremos a distribuição dos dados via trigger para bancos remotos, portanto criaremos uma função para criação dos inserts.
A função make_insert faz engenharia reversa na ddl e nos dados utilizando o módulo PL disponível no plruby
CREATE OR REPLACE FUNCTION make_insert(users) RETURNS text AS $RUBY$ row=args[0] tn=PL.args_type.to_s cn=row.keys.join(',') values= row.values.map{|m| (m)?"'#{m}'":'NULL' }.join(',') "INSERT INTO #{tn}(#{cn}) values (#{values})" $RUBY$ language plruby;
Veja o exemplo da execução
ruby_test=# select make_insert(users) from users;
Resultado
INSERT INTO users(name,id,password,login,email) values ('Fulano de tal','1','fulanopwd','fulano','fulano@server.com')
INSERT INTO users(name,id,password,login,email) values ('Maria da Silva','2','mariapwd','maria','maria.silva@server.com')
INSERT INTO users(name,id,password,login,email) values ('Sem nome','3','semnomepwd','semnome','semnome@server.com')
Sem aquela concatenação demasiada do plpgsql usando pipes e aspas para todo lado. Ah! a função make_insert é genérica para qualquer tabela mas no argumento de criação a sua tabela. Tipo
CREATE OR REPLACE FUNCTION make_insert(sua_tabela) RETURNS text AS $RUBY$ ...
Semelhante aos parâmentos tabela%rowtype do postgres.
Distribuindo os dados
Já vou dizendo logo, isso não é comparável ao Slony-I, isso é uma simples utilização do dblink do postgresql.
As funções de dblink ficam no pacote contrib do postgres, a instalação com apt-get
apt-get install postgresql-contrib-8.1
Crie as funções com \i onde o argumento é o arquivo dblink.sql
ruby_test=# \i /usr/share/postgresql/8.1/contrib/dblink.sql
Lembrando a assinatura da função dblink_exec é
select dblink_exec("connection string","query")
Exemplo
select dblink_exec('hostaddr=127.0.0.1 dbname=mydb user=shairon password=senha','insert into...');
Partiremos da seguinte extrategia: criaremos uma tabela chamada nodes onde armazenaremos os dados dos bancos de dados, selecionaremos apenas os nodes ativos e enviaremos os registros para os mesmos através da trigger replica_plruby(T) no evento after insert na tabela users master(A).
Veja o diagrama abaixo.

Então temos a tabela nodes.
create table nodes(id serial not null primary key, host varchar(80), username varchar(80), password varchar(80), active boolean )
Dados para teste
insert into nodes(host,dbname,username,password,active) values ('127.0.0.1','mydb1','foo','bar',true);
insert into nodes(host,dbname,username,password,active) values ('192.168.0.7','mydb2','foo','bar',true);
insert into nodes(host,dbname,username,password,active) values ('10.0.0.38','mydb3','foo','bar',true);
Criando a trigger replica_plruby, acompanhe pelos comentários
CREATE OR REPLACE FUNCTION replica_plruby() RETURNS trigger AS $RUBY$ #obtendo a quantidade de nós ativos count=PL::exec("select count(*) from nodes where active=true",1,"value").to_s.to_i if count > 0 #obtendo os nós ativos PL::Plan.new("select * from nodes where active=true").each do |row| #montando a conexão #cada row é um hash tipo {"dbname"=>"delme", "username"=>"shairon" ...} conn="hostaddr=#{row['host']} dbname=#{row['dbname']} user=#{row['username']} password=#{row['password']}" #Nome das culunas via metadata keys = new.keys.join(",") #Os valores com tratamento de nulo values = new.values.map{|m| (m)?"''#{m}''":'NULL' }.join(',') #Query final query = "INSERT INTO #{tg['relname']}(#{keys}) values (#{values})" #Disparo PL.exec("select dblink_exec('#{conn}','#{query}')") end end new $RUBY$ LANGUAGE plruby;
Pronto! Insira novos dados na tabela users para ver o resultado.
Referências
Nos arquivos de documentação dos pacotes
ruby1.8 postgresql-8.1 postgresql-8.1-plruby postgresql-contrib-8.1