Works by

Ren's blog

@rennnosuke_rk 技術ブログです

【Vue.js】Event Busによる親コンポーネントから子コンポーネントへのイベント伝播

f:id:rennnosukesann:20181217223208p:plain:w300

一般に、Vueで親コンポーネントから子コンポーネントへのプロパティ操作を行う場合、親コンポーネントの値を子コンポーネントにバインドします。
では子コンポーネントのメソッド操作を行う場合はどうするかというと、直接的には $refs を通して子コンポーネントのメソッド呼び出しを行う方法があります。

// 子
const child = new Vue({
  methods: {
    hoge: function () {
      console.log('hoge...');
    }
  },
  template: '<template>child.</template>'
})

// 親
const parent = new Vue({
  components: {
    child
  },
  methods: {
    hoge: function () {
      this.$refs.child.hoge(); // 子コンポーネントメソッド呼び出し
    }
  },
  template: '<template><child ref="child"/></template>'
})

しかしながら、このような直接呼び出しは公式でも示されている通り非推奨です

代わりに、Event Busを用いた子コンポーネント処理呼び出しの方法があります。

// EventBus
const bus = new Vue();

// 子
const child = new Vue({
  created: function () {
    bus.$on('hoge', this.hoge); // hogeイベントハンドラ登録
  }
  methods: {
    hoge: function () {
      console.log('hoge...');
    }
  },
  template: '<template>child.</template>'
})

// 親
const parent = new Vue({
  components: {
    child
  },
  methods: {
    hoge: function () {
      bus.$emit('hoge'); // hogeイベント発火
    }
  },
  template: '<template><child ref="child"/></template>'
})

Event Bus自体は単純Vueコンポーネントオブジェクトです。
EventBusの $on メソッドでイベントハンドラの登録を行います。第2引数にはイベント発火時に呼び出される関数を登録します。
イベントの発火はEventBusの $emit を呼び出して行います。

コンポーネントで予めイベントハンドラとして呼び出される関数を登録しておき、
コンポーネントでは子コンポーネント関数を呼び出したいタイミングでイベントの発火を行います。

このようなイベント駆動によってメソッド呼び出しを行うことで、子コンポーネントの実装による親コンポーネントの実装の変更は発生しにくく、コンポーネント間の依存度を低く抑えることができます。 ただし、予め発生するイベントシグナルについては親子間で合意しておく必要があります。


小規模アプリのイベント伝播ではEventBusが役に立ちます。
ただし、中規模以上のアプリケーションを使用する場合はEventBusに登録するイベントが氾濫してしまうので、Vuex を使用した状態管理/イベント伝播を使用すると良いと思います。

【Vue.js】Vue + TypeScript + vue-property-decoratorでのクラスメンバの可視性はどうするべきか?

f:id:rennnosukesann:20181217223208p:plain:w300

Vue + Typescript + vue-property-decoratorでコンポーネントをクラス宣言的に記述する際、data はクラスのインスタンス変数として定義し、
method はクラスのメソッドとして記述できます。

<template>
    <p v-for="(b, i) in books" :key="i">{{b.name}}</p>
</template>

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator';
import { BookFactory } from '@/service/factory/book';

@Component
export default class BookNames extends Vue {
    books: Book[] = [];

    fetchBooks(){
        books = BookFactory.findBooks();
    }
}
</script>

このとき、これらクラスメンバのアクセス修飾子はどのようにすればよいのか気になりました。

通常のTypeScriptクラス同様、コンポーネントクラスではアクセス修飾子を設定できます。

    private books: Book[] = [];

    private fetchBooks(){
        books = BookFactory.findBooks();
    }

この修飾子をprivateにしようがpublicにしようが、テンプレートからは参照できます。

<template>
    <p v-for="(b, i) in books" :key="i">{{b.name}}</p>
</template>

では、ref` を使用した外部コンポーネントからの呼び出しではどうなるのでしょうか。

<template>
    <book-names ref="books"/>
</template>

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator';
import { BookNames } from '@/components/molecules/BookNames';

@Component({
    components:{
        BookNames
    }
})
export default class Parent extends Vue {
    mounted(){
        console.log(this.$ref.books.books); // [ {name: "hoge"}, ... ]
    }
}
</script>

privateでも呼べてしまいます。外部コンポーネントからの呼び出しでも可視性は無視されました。*1

結局、TSをJSにトランスパイルする際にはTSの可視性は無視される(トランスパイルの際の構文チェックでのみ可視性が有効である)ため、
TS(Vue)の静的構文チェックで引っかからない限りアクセス修飾子は意味を成しません。

ex. privateメンバを持つTypeScriptクラスのトランスパイル

export default class Child {
    private hoge: string = "hoge";
}

"use strict";
exports.__esModule = true;
var Child = /** @class */ (function () {
    function Child() {
        this.hoge = "hoge";
    }
    return Child;
}());
exports["default"] = Child;

ただし、$refs の持つメンバへの型定義を無理やり行えば、クラスの可視性は有効になります。

export interface Context {
    readonly refs: {
        [key: string]: BookName;
    };
}

ただし、これでは $refsBookName しか参照できなくなります。


特に意味を成さないのであれば、privateはクラス内参照、修飾子なし/publicであれば外部参照用のメソッドなど、あくまで読み手に対するマーカとして使用していくのがベターなんですかね。。。

*1:本当は、親から子の場合でもコンポーネントへのメンバに直接アクセスすべきではありません。

【Vue.js】親コンポーネントから子コンポーネントへの基本型props値の動的伝播

f:id:rennnosukesann:20181217223208p:plain:w300

コンポーネントから子コンポーネントへデータを受け渡す手段として props を使用できます。
これは非常にベーシックかつ便利な手段で、動的なデータであっても親コンポーネントの変更があれば子コンポーネントにその状態変化を伝えることができます。
(ただし子コンポーネントから親コンポーネントへの伝播はご法度です。変更そのものはできますが、Warningが発生します)

コンポーネント側テンプレート
<!--postsはオブジェクト{id: number, title: string}の配列-->
<template v-for="p in posts"><blog-content post="p"/></template>
コンポーネント側定義
Vue.component('blog-content', {
  props: ['post'],
  template: '<template>id:{{post.id}} title:{{post.title}}</template>'
});

ただし、基本型を渡すときはその値がコピーとして渡るので注意が必要です。
例えば、以下のようなプロパティ渡しでは、初回の値こそ共有されますが、変更後の値は子コンポーネントへは反映されません。

コンポーネント側テンプレート
<!--isEnabledの型はboolean-->
<template>
    <my-dialog :enable="isEnabled" />
</template>
コンポーネント側定義
Vue.component('my-dialog', {
  props: ['enable'],
  template: '<template v-if="enable">dialog</template>'
});

この場合、基本型をオブジェクトでラップし、生成したオブジェクトへの参照を渡すことで動的変更内容を反映させます。

コンポーネント側テンプレート
<template>
    <my-dialog :enable="{ isEnabled }" />
</template>
コンポーネント側定義
Vue.component('my-dialog', {
  props: ['enable'],
  template: '<template v-if="enable.isEnabled">dialog</template>'
});

【Spring】Spring BootでのデフォルトのConponent Scan

f:id:rennnosukesann:20181220183020p:plain:w300

Spring Initializrで生成したばかりのSpring Bootプロジェクトからアプリを実行すると、Component Scanの対象となるパッケージがデフォルトでトップレベルから実行されていることがわかります。

これは、下記の様な @SpringBootApplication アノテーションが付与されたクラスがトップレベルのパッケージに配置されているためです。

// PlaygroundApplication.kt

@SpringBootApplication
class PlaygroundApplication

fun main(args: Array<String>) {
    runApplication<PlaygroundApplication>(*args)
}

@SpringBootApplication アノテーションには @ComponentScan アノテーションが付与されており、その引数には basePackages 指定がありません。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),  // basePackage指定なし
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
@ConfigurationPropertiesScan
public @interface SpringBootApplication {
...
}

basePackages 指定のない @ComponentScan は、アノテーションを付与したクラスが属するパッケージ階層以下をスキャン対象とします。
そのため、例えば PlaygroundApplication クラス(kotlinであればこのクラスを含む.ktファイル)が下図のように配置されている場合、そのComponent Scan対象は PlaygroundApplication が属するパッケージ階層以下となります。

com
└── springapp
    ├── PlaygroundApplication.kt // PlaygroundApplicationクラスを定義
    ├── ServletInitializer.kt
    ├── application
    │   └── config
    │       └── AppConfig.kt
    └── domain
        ├── model
...

この理由から、 PlaygroundApplication.kt を別のパッケージ階層に移動すると、そのパッケージ配下のコンポーネントだけがスキャン対象となり、上位のパッケージ及び別階層のパッケージのスキャンは実施されません。

com
└── springapp
    ├── ServletInitializer.kt
    ├── app
    │   └── PlaygroundApplication.kt
    ├── application
    │   └── config
    │       └── AppConfig.kt // スキャンされない!
    └── domain
        ├── model
***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in com.springapp.domain.service.UserService required a bean of type 'com.springapp.domain.repository.UserRepository' that could not be found.

The injection point has the following annotations:
    - @org.springframework.beans.factory.annotation.Autowired(required=true)
...

なお @SpringBootApplication で明示的にスキャン対象を指定する場合、 scanBasePackages プロパティを使用します。

@SpringBootApplication(scanBasePackages={"com"})
class PlaygroundApplication

fun main(args: Array<String>) {
    runApplication<PlaygroundApplication>(*args)
}

参考文献

docs.spring.io

【Spring】@AuthenticationPrincipal経由でセッションユーザ情報を取得する際のチェック事項

f:id:rennnosukesann:20181220183020p:plain:w300

@AuthenticationPrincipal

Spring Securityでは、セッションユーザ情報をシンプルに取得できるアノテーション @AuthenticationPrincipal が存在します。

Javadocより

メソッドのパラメーターまたはメソッドの戻り値に Authentication.getPrincipal()をバインドするアノテーションです。

型が UserDetails 実装型であるメソッドの引数、あるいは戻り値に付与することで、Authentication.getPrincipal()の呼び出し結果を埋め込んでくれるアノテーションです。

UserDetails はSpring Securityにおいてユーザ被認証主体の情報を保持する役割を持ち、Spring Security上では UserDetails の持つ情報を使って認証・認可の仕組みを提供しますが、その UserDetails オブジェクトを簡単に取得できます。

よくあるのが、Controllerメソッドの引数に埋め込んでログイン済みユーザ情報を使用するシーンです。

@RestController
@RequestMapping("/users")
class UserController {
    /**
     * ユーザ取得 - 【GET】/users/me
     */
    @GetMapping("/me")
    fun get(@AuthenticationPrincipal user: User): UserResponse = UserResponse.create(user) // Userからユーザ情報レスポンスを作成

}

上記Controllerクラスではログインユーザ情報を取得するAPIを実装していますが、
その際に @AuthenticationPrincipal で取得したログインユーザ情報を使用しています。

@AuthenticationPrincipal を使用するための準備

このように便利なアノテーションですが、利用のために幾つか下準備が必要です。
よく失念するので、手順の再確認も兼ねてメモしました。

UserDetails 実装クラスの定義

何はともあれインスタンスのクラスがないことには始まりません。
Spring Securityでのユーザ認証・認可を利用する場合、既に作成していることと思います。

data class User(
    val email: String,
    val pass: String,
    val roleType: RoleType,
) : UserDetails {

    override fun getAuthorities(): MutableCollection<out GrantedAuthority> {
        return AuthorityUtils.createAuthorityList(roleType.name)
    }

    override fun isEnabled(): Boolean {
        return true
    }

    override fun getUsername(): String {
        return email
    }

    override fun isCredentialsNonExpired(): Boolean {
        return true
    }

    override fun getPassword(): String {
        return pass
    }

    override fun isAccountNonExpired(): Boolean {
        return true
    }

    override fun isAccountNonLocked(): Boolean {
        return true
    }

}

UserDetailsService 実装の定義

UserDetails オブジェクトをハンドルするサービスクラスも定義します。
既にSpring Securityが提供している UserDetailsServiceを実装したServiceクラスを定義します。
loadUserByUsername をオーバーライドし、先程作成した UserDetails 実装クラスオブジェクトを提供するロジックを記述します。

@Service
class UserDetailsServiceImpl : UserDetailsService {

    @Throws(UsernameNotFoundException::class)
    override fun loadUserByUsername(email: String?): UserDetails {
        // Userクラスオブジェクトを返す
    }

}

WebSecurityConfigurerAdapter 派生クラスで UserService を登録

WebSecurityConfigurerAdapter 派生クラスを定義し、 configure メソッド内で先程の UserService を登録します。
派生クラスはBeanとして登録し、@EnableWebSecurity も忘れずに。

@Configuration
@EnableWebSecurity
class SecurityConfig(
    @Autowired private val userDetailsService: UserDetailsService,
) : WebSecurityConfigurerAdapter() {

    override fun configure(auth: AuthenticationManagerBuilder?) {
        auth?.userDetailsService(userDetailsService)
    }
...
}

呼び出し

あとは実際に呼び出してみます。null でなければOK。

@RestController
@RequestMapping("/users")
class UserController {
    /**
     * ユーザ取得 - 【GET】/users/me
     */
    @GetMapping("/me")
    fun get(@AuthenticationPrincipal user: User): UserResponse = UserResponse.create(user) // Userからユーザ情報レスポンスを作成

}

参考文献

docs.spring.io

【Cloud Foundry】Cloud FoundryアプリケーションのログをSyslog経由でLogDNAに転送する

f:id:rennnosukesann:20190205125549p:plain:w300

Cloud Foundly上のアプリケーションログをLogDNAに転送し、可視化してみます。

1. LogDNAのSyslogパスを取得する

LogDNAダッシュボードに移動し、「All Apps」->「Add apps」->「syslog」をクリックします。 するとSyslogのパスが表示されるので、これをコピー。 (初回はパスが表示されていないので、パス表示部分をクリック)

f:id:rennnosukesann:20190903114438p:plain:w500

あるいは、ログダッシュボード画面左下のヘルプボタン->「syslog」からも開けます。

f:id:rennnosukesann:20190904195024p:plain:w300

2. CF上にSyslog転送サービスを作成

Syslog経由でログを転送するサービスを作成します。

$ cf cups [Syslog転送サービス名] -l [Syslogパス名]

ex.

$ cf cups syslog-transfer -l syslog://.....:XXXXX

3. CF上アプリケーションにサービスをバインド

次に、作成したサービスをアプリにバインドします。

$ cf bind-service [CFアプリ名] [Syslog転送サービス名]

ex.

$ cf bind-service binded-app syslog-transfer

4. アプリを再始動

cf bind-service 実行後、標準出力の案内に従いアプリを再始動します。

 $ cf restage [CFアプリ名]

ex.

$ cf restage  binded-app

しばらくすると、LogDNA側にログが吐き出されます。

参考文献

logdna.com

【JPA】@MappedSuperClassでEntityクラスの共通カラムプロパティをまとめる

@MappedSuperClass

主キー用IDや最終更新日時など、RDB上の各テーブルには同一定義のカラムが存在することが少なくありません。
これがJPAのEntityにマッピングされると、複数のEntityクラスに同一定義のカラムプロパティが散在することになります。

(ソースはKotlin)

User.kt
@Entity
@Table(name = "USERS")
@Where(clause="IS_DELETED=0")
data class User(

    // 頻出
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID", nullable = false)
    var id: Int? = null,

    @Column(name = "NAME", nullable = false)
    val name: String = "",

    @Column(name = "EMAIL", nullable = false)
    val email: String = "",

    // 頻出
    @Column(name = "CREATED_AT", nullable = false)
    val createdAt: Date = Date(),

    // 頻出
    @Column(name = "UPDATED_AT", nullable = false)
    val updatedAt: Date = Date(),

    // 頻出
    @Column(name = "IS_DELETED", nullable = false)
    var isDeleted: Boolean = false

)
Article.kt
@Entity
@Table(name = "ARTICLES")
@Where(clause = "IS_DELETED=0")
data class Article(

    // 頻出
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID", nullable = false)
    var id: Int? = null,

    @Column(name = "USER_ID", nullable = false)
    val userId: Int? = null,

    @Column(name = "TITLE", nullable = false)
    val title: String = "",

    @Column(name = "DESCRIPTION", nullable = false)
    val description: String = "",

    // 頻出
    @Column(name = "CREATED_AT", nullable = false)
    val createdAt: Date = Date(),

    // 頻出
    @Column(name = "UPDATED_AT", nullable = false)
    val updatedAt: Date = Date(),

    // 頻出
    @Column(name = "IS_DELETED", nullable = false)
    var isDeleted: Boolean = false,

    @ManyToOne(targetEntity = User::class, cascade = [], fetch = FetchType.EAGER, optional = true)
    @JoinColumn(name = "USER_ID", referencedColumnName = "ID", insertable = false, updatable = false)
    val user: User? = null

)

@MappedSuperClass で修飾されたクラスを定義し、その中に共通カラムプロパティを定義した上で、各Entityクラスで継承することで、共通のカラムを派生Entity先クラスで書かずに済みます。

@MappedSuperClass で修飾されたクラス自体のテーブルへのマッピングは行われません)

TransactionalEntity
/**
 * 共通するカラムのプロパティをここで定義
 */
@MappedSuperclass
abstract class TransactionalEntity(

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID", nullable = false)
    var id: Int? = null,

    @Column(name = "CREATED_AT", nullable = false)
    val createdAt: Date = Date(),

    @Column(name = "UPDATED_AT", nullable = false)
    val updatedAt: Date = Date(),

    @Column(name = "IS_DELETED", nullable = false)
    var isDeleted: Boolean = false

)
User.kt(修正後)
@Entity
@Table(name = "USERS")
@Where(clause="IS_DELETED=0")
data class User(

    @Column(name = "NAME", nullable = false)
    val name: String = "",

    @Column(name = "EMAIL", nullable = false)
    val email: String = "",

    @OneToMany(targetEntity = User::class, cascade = [], fetch = FetchType.LAZY, mappedBy = "id")
    val articles: List<Article>? = null

) : TransactionalEntity() // TransactionalEntityを継承する
Article.kt(修正後)

@Entity
@Table(name = "ARTICLES")
@Where(clause = "IS_DELETED=0")
data class Article(

    @Column(name = "USER_ID", nullable = false)
    val userId: Int? = null,

    @Column(name = "TITLE", nullable = false)
    val title: String = "",

    @Column(name = "DESCRIPTION", nullable = false)
    val description: String = "",

    @ManyToOne(targetEntity = User::class, cascade = [], fetch = FetchType.EAGER, optional = true)
    @JoinColumn(name = "USER_ID", referencedColumnName = "ID", insertable = false, updatable = false)
    val user: User? = null

) : TransactionalEntity() // TransactionalEntityを継承する

継承の是非等ありますが、かなりスッキリとしたEntityが書けるようになります。