Fullmenu null

 

09 December 2017

En este post vamos a analizar un caso de negocio que seguramente no ocurre muy a menudo pero que espero nos ayude a resolver situaciones parecidas. En este caso vamos a desarrollar un script que se podrá ejecutar repetidas veces a lo largo del tiempo (cada hora, varias veces al día, etc) cuya lógica de negocio queremos que se mantenga durante la ejecución de las mismas.

Nuestro Community Manager siente curiosidad por ciertos usuarios de Twitter que nos siguen en nuestra cuenta oficial y quiere hacer un pequeño estudio sobre los mismos que va a durar un cierto tiempo (dias/semanas/meses). Durante este tiempo nos irá proporcionando usuarios de Twitter de los que tendremos que obtener ciertos datos públicos e ir guardándolos. Así mismo durante este período nos irá pidiendo que le hagamos un pequeño informe con los datos guardados.

En un escenario convencional, podríamos optar por escribir en un fichero plano la información de cada ejecución o si queremos algo más robusto instalaríamos un motor de base de datos tipo MySQL, Postgre, SQLServer, etc. Mientras que lo primero es muy simple de mantener resulta muy complejo de gestionar/programar. Por el contrario la segunda opción es mucho más robusta pero más compleja.

A medio de camino de ambas soluciones contamos con proyectos como SQLite que nos permitirán crear y acceder a los datos mediante un acceso JDBC (base de datos) pero sin las complejidades de tener que instalar y mantener un motor de datos más completo.

Dependencias

Para este scritp necesitaremos acceder a Twitter y a una base de datos SQLite por lo que definimos las dependencias:

@GrabConfig(systemClassLoader=true)
@Grab(group='org.twitter4j', module='twitter4j-core', version='4.0.6')
@Grab(group='org.xerial', module='sqlite-jdbc', version='3.21.0')
import groovy.sql.Sql
import twitter4j.Twitter;
import twitter4j.TwitterFactory;
import twitter4j.StatusUpdate

Argumentos

El script admitirá la siguiente lista de argumentos:

  • --initialize, recrea la base de datos

  • --username, busca un usuario proporcionado por parámetro y lo incluye en la base de datos

  • --all, recorre todos los usuarios existentes en la base de datos y los actualiza

  • --report, genera un informe con los datos guardados hasta la fecha

def cli = new CliBuilder(usage: '-i -u username -r')
cli.with {
    h(longOpt: 'help',    args:0,'Usage Information', required: false)
    i(longOpt: 'initialize',args:0,'Borrar todos los datos y crear la base de datos en limpio', required: false)
    u(longOpt: 'username', args:1, argName:'username', 'El usuario a investigar', required: false)
    a(longOpt: 'all',args:0,'Actualiza la info de todos los usuarios registrados', required: false)
    r(longOpt: 'report',args:0,'Genera un report con los usuarios existentes', required: false)
}
def options = cli.parse(args)
if (options.h  ) {
    cli.usage()
    return
}

Modelo

Para ayudar en la encapsulación de los datos definimos una clase TwitterUser la cual se auto-actualiza si usamos el constructor que proporciona un id de Twitter:

class TwitterUser{
    String id
    String name
    int tweets
    int followers
    int friends
    String timeZone

    TwitterUser(){
    }

    TwitterUser( String id){
        def tuser = TwitterFactory.singleton.users().showUser(id)
        this.id=id
        this.name = tuser.name
        this.tweets = tuser.statusesCount
        this.followers = tuser.followersCount
        this.friends = tuser.friendsCount
        this.timeZone = tuser.timeZone
    }
}

Prepare

Antes de realizar ninguna acción contra la base de datos el propio script realiza una preparación de la misma. Si se indicó el parámetro --initialize realizará un borrado de la base de datos. En cualquier caso creará la tabla si no existe

void prepareDatabase(boolean drop) {
    Sql sql = Sql.newInstance("jdbc:sqlite:tweetreport.db")
    if( drop )
        sql.execute "drop table if exists tweetreport"
    String sentence = """CREATE table if not EXISTS tweetreport
        (id varchar(20), name VARCHAR(40), tweets NUMBER(5), followers NUMBER(5), friends number(5), timezone varchar(10))
    """
    sql.execute sentence
}

Update User

Si se indica un usuario, el script creará un objeto de la clase TwitterUser y mediante una simple select por su id determinará si existe ya. Si existe se procederá a actualizar el registro mientras que si no existe se insertará un registro nuevo

void updateUser(TwitterUser user){
    Sql sql = Sql.newInstance("jdbc:sqlite:tweetreport.db")
    def row = sql.firstRow("select * from tweetreport where id=?",[user.id])
    String sentence = row ?
            "update tweetreport set name=?.name, tweets=?.tweets, followers=?.followers, friends=?.friends, timezone=?.timeZone where id=?.id"
            :
            "insert into tweetreport (id,name,tweets,followers,friends,timezone) values (?.id,?.name,?.tweets,?.followers,?.friends,?.timeZone)"
    sql.execute sentence, user
}

Mediante la opción --all hacemos que el script recorra todos los usuarios guardados hasta la fecha e invoque el método upate para cada uno de ellos

Report User

En cualquier momento podemos solicitar que el script genere un informe con el estado de la base de datos, lo cual en nuestro ejemplo consistirá en recorrer los registros y mostrarlos por consola:

void report(){
    Sql sql = Sql.newInstance("jdbc:sqlite:tweetreport.db")
    sql.eachRow"select * from tweetreport order by id",{ row->
        println row
    }
}

Persistencia

Al ejecutar el script se creará un fichero tweetreport.db (indicado en la cadena de conexión Sql) que SQLite utilizará para ofrecer el servicio de persistencia.

De esta forma simple podremos dotar a nuestros scritps de la capacidad de tener una base de datos sencilla y sin complejidades de instalación ni administración


Script
//tag::dependencies[]
@GrabConfig(systemClassLoader=true)
@Grab(group='org.twitter4j', module='twitter4j-core', version='4.0.6')
@Grab(group='org.xerial', module='sqlite-jdbc', version='3.21.0')
import groovy.sql.Sql
import twitter4j.Twitter;
import twitter4j.TwitterFactory;
import twitter4j.StatusUpdate
//end::dependencies[]

//tag::model[]
class TwitterUser{
    String id
    String name
    int tweets
    int followers
    int friends
    String timeZone

    TwitterUser(){
    }

    TwitterUser( String id){
        def tuser = TwitterFactory.singleton.users().showUser(id)
        this.id=id
        this.name = tuser.name
        this.tweets = tuser.statusesCount
        this.followers = tuser.followersCount
        this.friends = tuser.friendsCount
        this.timeZone = tuser.timeZone
    }
}
//end::model[]

//tag::prepare[]
void prepareDatabase(boolean drop) {
    Sql sql = Sql.newInstance("jdbc:sqlite:tweetreport.db")
    if( drop )
        sql.execute "drop table if exists tweetreport"
    String sentence = """CREATE table if not EXISTS tweetreport
        (id varchar(20), name VARCHAR(40), tweets NUMBER(5), followers NUMBER(5), friends number(5), timezone varchar(10))
    """
    sql.execute sentence
}
//end::prepare[]

//tag::update[]
void updateUser(TwitterUser user){
    Sql sql = Sql.newInstance("jdbc:sqlite:tweetreport.db")
    def row = sql.firstRow("select * from tweetreport where id=?",[user.id])
    String sentence = row ?
            "update tweetreport set name=?.name, tweets=?.tweets, followers=?.followers, friends=?.friends, timezone=?.timeZone where id=?.id"
            :
            "insert into tweetreport (id,name,tweets,followers,friends,timezone) values (?.id,?.name,?.tweets,?.followers,?.friends,?.timeZone)"
    sql.execute sentence, user
}
//end::update[]

List listUsers(){
    Sql sql = Sql.newInstance("jdbc:sqlite:tweetreport.db")
    sql.rows("select id from tweetreport")*.id
}

//tag::report[]
void report(){
    Sql sql = Sql.newInstance("jdbc:sqlite:tweetreport.db")
    sql.eachRow"select * from tweetreport order by id",{ row->
        println row
    }
}
//end::report[]

//tag::cli[]
def cli = new CliBuilder(usage: '-i -u username -r')
cli.with {
    h(longOpt: 'help',    args:0,'Usage Information', required: false)
    i(longOpt: 'initialize',args:0,'Borrar todos los datos y crear la base de datos en limpio', required: false)
    u(longOpt: 'username', args:1, argName:'username', 'El usuario a investigar', required: false)
    a(longOpt: 'all',args:0,'Actualiza la info de todos los usuarios registrados', required: false)
    r(longOpt: 'report',args:0,'Genera un report con los usuarios existentes', required: false)
}
def options = cli.parse(args)
if (options.h  ) {
    cli.usage()
    return
}
//end::cli[]

prepareDatabase(options.i )

if( options.username ) {
    TwitterUser user = new TwitterUser(options.username)
    updateUser(user)
}
if( options.all ){
    listUsers().each{ id->
        println "update $id"
        TwitterUser user = new TwitterUser(id)
        updateUser(user)
    }
}
if( options.report ){
    report()
}